)]}'
{"version": 3, "sources": ["/web/static/src/legacy/js/core/class.js", "/web/static/src/legacy/js/public/minimal_dom.js", "/web/static/src/legacy/js/public/public_widget.js", "/web_editor/static/lib/cropperjs/cropper.js", "/web_editor/static/lib/jquery-cropper/jquery-cropper.js", "/web_editor/static/lib/jQuery.transfo.js", "/web_editor/static/lib/webgl-image-filter/webgl-image-filter.js", "/web/static/lib/dompurify/DOMpurify.js", "/web_editor/static/src/js/editor/odoo-editor/src/OdooEditor.js", "/web_editor/static/src/js/editor/odoo-editor/src/utils/constants.js", "/web_editor/static/src/js/editor/odoo-editor/src/utils/sanitize.js", "/web_editor/static/src/js/editor/odoo-editor/src/utils/serialize.js", "/web_editor/static/src/js/editor/odoo-editor/src/tablepicker/TablePicker.js", "/web_editor/static/src/js/editor/odoo-editor/src/powerbox/patienceDiff.js", "/web_editor/static/src/js/editor/odoo-editor/src/powerbox/Powerbox.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/align.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/commands.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteBackward.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/deleteForward.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/enter.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/shiftEnter.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/shiftTab.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/tab.js", "/web_editor/static/src/js/editor/odoo-editor/src/commands/toggleList.js", "/web_editor/static/src/js/editor/drag_and_drop.js", "/web_editor/static/src/js/wysiwyg/linkDialogCommand.js", "/web_editor/static/src/js/wysiwyg/MoveNodePlugin.js", "/web_editor/static/src/js/wysiwyg/PeerToPeer.js", "/web_editor/static/src/js/wysiwyg/conflict_dialog.js", "/web_editor/static/src/js/wysiwyg/get_color_picker_template_service.js", "/web_editor/static/src/js/editor/perspective_utils.js", "/web_editor/static/src/js/editor/image_processing.js", "/web_editor/static/src/js/editor/custom_colors.js", "/web_editor/static/src/js/wysiwyg/widgets/alt_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/chatgpt_alternatives_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/chatgpt_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/chatgpt_prompt_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/chatgpt_translate_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/color_palette.js", "/web_editor/static/src/js/wysiwyg/widgets/image_crop.js", "/web_editor/static/src/js/wysiwyg/widgets/link.js", "/web_editor/static/src/js/wysiwyg/widgets/link_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/link_popover_widget.js", "/web_editor/static/src/js/wysiwyg/widgets/link_tools.js", "/web_editor/static/src/js/editor/toolbar.js", "/web_editor/static/src/js/editor/add_snippet_dialog.js", "/web_editor/static/src/js/wysiwyg/wysiwyg_jquery_extention.js", "/web_editor/static/src/js/wysiwyg/wysiwyg.js", "/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js", "/website/static/src/js/editor/editor.js", "/website/static/src/js/editor/add_snippet_dialog.js", "/website/static/src/js/editor/widget_link.js", "/web_editor/static/src/js/core/owl_utils.js", "/web_editor/static/src/js/editor/snippets.editor.js", "/web_editor/static/src/js/editor/snippets.options.js", "/website/static/src/js/editor/snippets.editor.js", "/website/static/src/js/editor/snippets.options.js", "/website/static/src/js/editor/shared_options/pricelist.js", "/website/static/src/snippets/s_facebook_page/options.js", "/website/static/src/snippets/s_image/options.js", "/website/static/src/snippets/s_image_gallery/options.js", "/website/static/src/snippets/s_instagram_page/options.js", "/website/static/src/snippets/s_card/options.js", "/website/static/src/snippets/s_faq_horizontal/options.js", "/website/static/src/snippets/s_carousel_intro/options.js", "/website/static/src/snippets/s_countdown/options.js", "/website/static/src/snippets/s_masonry_block/options.js", "/website/static/src/snippets/s_popup/options.js", "/website/static/src/snippets/s_chart/options.js", "/website/static/src/snippets/s_rating/options.js", "/website/static/src/snippets/s_tabs/options.js", "/website/static/src/snippets/s_progress_bar/options.js", "/website/static/src/snippets/s_table_of_content/options.js", "/website/static/src/snippets/s_timeline/options.js", "/website/static/src/snippets/s_media_list/options.js", "/website/static/src/snippets/s_google_map/options.js", "/website/static/src/snippets/s_map/options.js", "/website/static/src/snippets/s_dynamic_snippet/options.js", "/website/static/src/snippets/s_dynamic_snippet_carousel/options.js", "/website/static/src/snippets/s_website_controller_page_listing_layout/options.js", "/website/static/src/snippets/s_website_form/options.js", "/website/static/src/js/form_editor_registry.js", "/website/static/src/js/send_mail_form.js", "/website/static/src/snippets/s_searchbar/options.js", "/website/static/src/snippets/s_social_media/options.js", "/website/static/src/snippets/s_process_steps/options.js", "/website/static/src/js/widgets/link_popover_widget.js", "/website/static/src/js/editor/commands_overridden.js", "/website/static/src/js/editor/odoo_editor.js", "/website_crm/static/src/js/website_crm_editor.js", "/website_helpdesk/static/src/js/website_helpdesk_form_editor.js", "/website_payment/static/src/snippets/s_donation/options.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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/4BA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACx/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;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;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;AChoBA;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACn+KA;;;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACpXA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACvQA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7YA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1jCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnSA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/PA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnqBA;;;;;;;;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;;;;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;;;;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;;;;AC/kBA;;;;;;;;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;;;;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;;;;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;;;;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;;;;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;;;;AC3DA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/iCA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;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;;;;AC1UA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC3lBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;ACrGA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC9qHA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7BA;;;;;;;;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;;;;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;;;;ACjDA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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/sKA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACptTA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACryBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC18IA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC1mBA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;AC1BA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;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;;;;AC/JA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5NA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;;;;ACrPA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;AC3XA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACh0DA;;;;;;;;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;;;;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;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACtZA;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;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;;;;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;;;;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;;;;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", "sourcesContent": ["/**\n * Improved John Resig's inheritance, based on:\n *\n * Simple JavaScript Inheritance\n * By John Resig http://ejohn.org/\n * MIT Licensed.\n *\n * Adds \"include()\"\n *\n * Defines The Class object. That object can be used to define and inherit classes using\n * the extend() method.\n *\n * Example::\n *\n *     var Person = Class.extend({\n *      init: function(isDancing){\n *         this.dancing = isDancing;\n *       },\n *       dance: function(){\n *         return this.dancing;\n *       }\n *     });\n *\n * The init() method act as a constructor. This class can be instanced this way::\n *\n *     var person = new Person(true);\n *     person.dance();\n *\n *     The Person class can also be extended again:\n *\n *     var Ninja = Person.extend({\n *       init: function(){\n *         this._super( false );\n *       },\n *       dance: function(){\n *         // Call the inherited version of dance()\n *         return this._super();\n *       },\n *       swingSword: function(){\n *         return true;\n *       }\n *     });\n *\n * When extending a class, each re-defined method can use this._super() to call the previous\n * implementation of that method.\n *\n * @class Class\n */\nfunction OdooClass(){}\n\nvar initializing = false;\n// eslint-disable-next-line no-undef\nvar fnTest = /xyz/.test(function(){xyz();}) ? /\\b_super\\b/ : /.*/;\n\n/**\n * Subclass an existing class\n *\n * @param {Object} prop class-level properties (class attributes and instance methods) to set on the new class\n */\nOdooClass.extend = function() {\n    var _super = this.prototype;\n    // Support mixins arguments\n    var args = [...arguments];\n    args.unshift({});\n\n    const prop = {};\n    args.forEach((arg) => {\n        Object.assign(prop, arg);\n    });\n\n    // Instantiate a web class (but only create the instance,\n    // don't run the init constructor)\n    initializing = true;\n    var This = this;\n    var prototype = new This();\n    initializing = false;\n\n    // Copy the properties over onto the new prototype\n    Object.keys(prop).forEach((name) => {\n        // Check if we're overwriting an existing function\n        prototype[name] = typeof prop[name] == \"function\" &&\n                          fnTest.test(prop[name]) ?\n                (function(name, fn) {\n                    return function() {\n                        var tmp = this._super;\n\n                        // Add a new ._super() method that is the same\n                        // method but on the super-class\n                        this._super = _super[name];\n\n                        // The method only need to be bound temporarily, so\n                        // we remove it when we're done executing\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n\n                        return ret;\n                    };\n                })(name, prop[name]) :\n                prop[name];\n    });\n\n    // The dummy class constructor\n    function Class() {\n        if(this.constructor !== OdooClass){\n            throw new Error(\"You can only instanciate objects with the 'new' operator\");\n        }\n        // All construction is actually done in the init method\n        this._super = null;\n        if (!initializing && this.init) {\n            var ret = this.init.apply(this, arguments);\n            if (ret) { return ret; }\n        }\n        return this;\n    }\n    Class.include = function (properties) {\n        Object.keys(properties).forEach((name) => {\n            if (typeof properties[name] !== 'function'\n                    || !fnTest.test(properties[name])) {\n                prototype[name] = properties[name];\n            } else if (typeof prototype[name] === 'function'\n                       && prototype.hasOwnProperty(name)) {\n                prototype[name] = (function (name, fn, previous) {\n                    return function () {\n                        var tmp = this._super;\n                        this._super = previous;\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n                        return ret;\n                    };\n                })(name, properties[name], prototype[name]);\n            } else if (typeof _super[name] === 'function') {\n                prototype[name] = (function (name, fn) {\n                    return function () {\n                        var tmp = this._super;\n                        this._super = _super[name];\n                        var ret = fn.apply(this, arguments);\n                        this._super = tmp;\n                        return ret;\n                    };\n                })(name, properties[name]);\n            }\n        });\n    };\n\n    // Populate our constructed prototype object\n    Class.prototype = prototype;\n\n    // Enforce the constructor to be what we expect\n    Class.constructor = Class;\n\n    // And make this class extendable\n    Class.extend = this.extend;\n\n    return Class;\n};\n\nexport default OdooClass;\n", "/** @odoo-module **/\n\nimport { addLoadingEffect } from '@web/core/utils/ui';\n\nexport const DEBOUNCE = 400;\nexport const BUTTON_HANDLER_SELECTOR = 'a, button, input[type=\"submit\"], input[type=\"button\"], .btn';\n\n/**\n * Protects a function which is to be used as a handler by preventing its\n * execution for the duration of a previous call to it (including async\n * parts of that call).\n *\n * Limitation: as the handler is ignored during async actions,\n * the 'preventDefault' or 'stopPropagation' calls it may want to do\n * will be ignored too. Using the 'preventDefault' and 'stopPropagation'\n * arguments solves that problem.\n *\n * @param {function} fct\n *      The function which is to be used as a handler. If a promise\n *      is returned, it is used to determine when the handler's action is\n *      finished. Otherwise, the return is used as jQuery uses it.\n * @param {function|boolean} preventDefault\n * @param {function|boolean} stopPropagation\n * @param {function|boolean} stopImmediatePropagation\n */\nexport function makeAsyncHandler(fct, preventDefault, stopPropagation, stopImmediatePropagation) {\n    let pending = false;\n    function _isLocked() {\n        return pending;\n    }\n    function _lock() {\n        pending = true;\n    }\n    function _unlock() {\n        pending = false;\n    }\n    return function (ev) {\n        if (preventDefault === true || preventDefault && preventDefault()) {\n            ev.preventDefault();\n        }\n        if (stopPropagation === true || stopPropagation && stopPropagation()) {\n            ev.stopPropagation();\n        }\n        if (stopImmediatePropagation === true || stopImmediatePropagation && stopImmediatePropagation()) {\n            ev.stopImmediatePropagation();\n        }\n\n        if (_isLocked()) {\n            // If a previous call to this handler is still pending, ignore\n            // the new call.\n            return;\n        }\n\n        _lock();\n        const result = fct.apply(this, arguments);\n        Promise.resolve(result).finally(_unlock);\n        return result;\n    };\n}\n\n/**\n * Creates a debounced version of a function to be used as a button click\n * handler. Also improves the handler to disable the button for the time of\n * the debounce and/or the time of the async actions it performs.\n *\n * Limitation: if two handlers are put on the same button, the button will\n * become enabled again once any handler's action finishes (multiple click\n * handlers should however not be bound to the same button).\n *\n * @param {function} fct\n *      The function which is to be used as a button click handler. If a\n *      promise is returned, it is used to determine when the button can be\n *      re-enabled. Otherwise, the return is used as jQuery uses it.\n * @param {function|boolean} preventDefault\n * @param {function|boolean} stopPropagation\n * @param {function|boolean} stopImmediatePropagation\n */\nexport function makeButtonHandler(fct, preventDefault, stopPropagation, stopImmediatePropagation) {\n    // Fallback: if the final handler is not bound to a button, at least\n    // make it an async handler (also handles the case where some events\n    // might ignore the disabled state of the button).\n    fct = makeAsyncHandler(fct, preventDefault, stopPropagation, stopImmediatePropagation);\n\n    return function (ev) {\n        const result = fct.apply(this, arguments);\n\n        const buttonEl = ev.target.closest(BUTTON_HANDLER_SELECTOR);\n        if (!(buttonEl instanceof HTMLElement)) {\n            return result;\n        }\n\n        // Disable the button for the duration of the handler's action\n        // or at least for the duration of the click debounce. This makes\n        // a 'real' debounce creation useless. Also, during the debouncing\n        // part, the button is disabled without any visual effect.\n        buttonEl.classList.add(\"pe-none\");\n        new Promise(resolve => setTimeout(resolve, DEBOUNCE)).then(() => {\n            buttonEl.classList.remove(\"pe-none\");\n            const restore = addLoadingEffect(buttonEl);\n            return Promise.resolve(result).then(restore, restore);\n        });\n\n        return result;\n    };\n}\n", "/**\n * Provides a way to start JS code for public contents.\n */\n\nimport { Component } from \"@odoo/owl\";\nimport Class from \"@web/legacy/js/core/class\";\nimport { loadBundle, loadCSS, loadJS } from '@web/core/assets';\nimport { SERVICES_METADATA } from \"@web/core/utils/hooks\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { makeAsyncHandler, makeButtonHandler } from \"@web/legacy/js/public/minimal_dom\";\n\n/**\n * Mixin to structure objects' life-cycles following a parent-children\n * relationship. Each object can a have a parent and multiple children.\n * When an object is destroyed, all its children are destroyed too releasing\n * any resource they could have reserved before.\n *\n * @name ParentedMixin\n * @mixin\n */\nconst ParentedMixin = {\n    __parentedMixin: true,\n\n    init: function () {\n        this.__parentedDestroyed = false;\n        this.__parentedChildren = [];\n        this.__parentedParent = null;\n    },\n    /**\n     * Set the parent of the current object. When calling this method, the\n     * parent will also be informed and will return the current object\n     * when its getChildren() method is called. If the current object did\n     * already have a parent, it is unregistered before, which means the\n     * previous parent will not return the current object anymore when its\n     * getChildren() method is called.\n     */\n    setParent(parent) {\n        if (this.getParent()) {\n            if (this.getParent().__parentedMixin) {\n                const children = this.getParent().getChildren();\n                this.getParent().__parentedChildren = children.filter(\n                    (child) => child.$el !== this.$el\n                );\n            }\n        }\n        this.__parentedParent = parent;\n        if (parent && parent.__parentedMixin) {\n            parent.__parentedChildren.push(this);\n        }\n    },\n    /**\n     * Return the current parent of the object (or null).\n     */\n    getParent() {\n        return this.__parentedParent;\n    },\n    /**\n     * Return a list of the children of the current object.\n     */\n    getChildren() {\n        return [...this.__parentedChildren];\n    },\n    /**\n     * Returns true if destroy() was called on the current object.\n     */\n    isDestroyed() {\n        return this.__parentedDestroyed;\n    },\n    /**\n     * Releases any resource the instance could have reserved.\n     */\n    destroy() {\n        this.getChildren().forEach(function (child) {\n            child.destroy();\n        });\n        this.setParent(undefined);\n        this.__parentedDestroyed = true;\n    },\n};\n\nfunction OdooEvent(target, name, data) {\n    this.target = target;\n    this.name = name;\n    this.data = Object.create(null);\n    Object.assign(this.data, data);\n    this.stopped = false;\n}\nOdooEvent.prototype.stopPropagation = function () {\n    this.stopped = true;\n};\nOdooEvent.prototype.is_stopped = function () {\n    return this.stopped;\n};\n\n/**\n * Do not ever use it directly, use EventDispatcherMixin instead. This class\n * just handles the dispatching of events, it is not meant to be extended, nor\n * used directly. All integration with parenting and automatic unregistration of\n * events is done in EventDispatcherMixin.\n *\n * Copyright notice for the following Class and its uses:\n *\n * (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.\n * Backbone may be freely distributed under the MIT license.\n * For all details and documentation:\n * http://backbonejs.org\n *\n * See the debian/copyright file for the text of the MIT license.\n */\nclass Events {\n    on(events, callback, context) {\n        var ev;\n        events = events.split(/\\s+/);\n        var calls = this._callbacks || (this._callbacks = {});\n        while ((ev = events.shift())) {\n            var list = calls[ev] || (calls[ev] = {});\n            var tail = list.tail || (list.tail = list.next = {});\n            tail.callback = callback;\n            tail.context = context;\n            list.tail = tail.next = {};\n        }\n        return this;\n    }\n    off(events, callback, context) {\n        var ev, calls, node;\n        if (!events) {\n            delete this._callbacks;\n        } else if ((calls = this._callbacks)) {\n            events = events.split(/\\s+/);\n            while ((ev = events.shift())) {\n                node = calls[ev];\n                delete calls[ev];\n                if (!callback || !node) {\n                    continue;\n                }\n                while ((node = node.next) && node.next) {\n                    if (node.callback === callback\n                            && (!context || node.context === context)) {\n                        continue;\n                    }\n                    this.on(ev, node.callback, node.context);\n                }\n            }\n        }\n        return this;\n    }\n    callbackList() {\n        var lst = [];\n        for (const [eventName, el] of Object.entries(this._callbacks || {})) {\n            var node = el;\n            while ((node = node.next) && node.next) {\n                lst.push([eventName, node.callback, node.context]);\n            }\n        }\n        return lst;\n    }\n    trigger(events) {\n        var event, node, calls, tail, args, all, rest;\n        if (!(calls = this._callbacks)) {\n            return this;\n        }\n        all = calls.all;\n        (events = events.split(/\\s+/)).push(null);\n        // Save references to the current heads & tails.\n        while ((event = events.shift())) {\n            if (all) {\n                events.push({\n                    next: all.next,\n                    tail: all.tail,\n                    event: event\n                });\n            }\n            if (!(node = calls[event])) {\n                continue;\n            }\n            events.push({\n                next: node.next,\n                tail: node.tail\n            });\n        }\n        rest = Array.prototype.slice.call(arguments, 1);\n        while ((node = events.pop())) {\n            tail = node.tail;\n            args = node.event ? [node.event].concat(rest) : rest;\n            while ((node = node.next) !== tail) {\n                node.callback.apply(node.context || this, args);\n            }\n        }\n        return this;\n    }\n}\n\n/**\n * Mixin containing an event system. Events are also registered by specifying\n * the target object (the object which will receive the event when raised). Both\n * the event-emitting object and the target object store or reference to each\n * other. This is used to correctly remove all reference to the event handler\n * when any of the object is destroyed (when the destroy() method from\n * ParentedMixin is called). Removing those references is necessary to avoid\n * memory leak and phantom events (events which are raised and sent to a\n * previously destroyed object).\n *\n * @name EventDispatcherMixin\n * @mixin\n */\nconst EventDispatcherMixin = Object.assign({}, ParentedMixin, {\n    __eventDispatcherMixin: true,\n    \"custom_events\": {},\n\n    init() {\n        ParentedMixin.init.call(this);\n        this.__edispatcherEvents = new Events();\n        this.__edispatcherRegisteredEvents = [];\n        this._delegateCustomEvents();\n    },\n    /**\n     * Proxies a method of the object, in order to keep the right ``this`` on\n     * method invocations.\n     *\n     * This method is similar to ``Function.prototype.bind``, and\n     * even more so to ``jQuery.proxy`` with a fundamental difference: its\n     * resolution of the method being called is lazy, meaning it will use the\n     * method as it is when the proxy is called, not when the proxy is created.\n     *\n     * Other methods will fix the bound method to what it is when creating the\n     * binding/proxy, which is fine in most javascript code but problematic in\n     * Odoo where developers may want to replace existing callbacks with theirs.\n     *\n     * The semantics of this precisely replace closing over the method call.\n     *\n     * @param {String|Function} method function or name of the method to invoke\n     * @returns {Function} proxied method\n     */\n    proxy(method) {\n        var self = this;\n        return function () {\n            var fn = (typeof method === 'string') ? self[method] : method;\n            if (fn === void 0) {\n                throw new Error(\"Couldn't find method '\" + method + \"' in widget \" + self);\n            }\n            return fn.apply(self, arguments);\n        };\n    },\n    _delegateCustomEvents() {\n        if (Object.keys(this.custom_events || {}).length === 0) {\n            return;\n        }\n        for (var key in this.custom_events) {\n            if (!Object.prototype.hasOwnProperty.call(this.custom_events, key)) {\n                continue;\n            }\n\n            var method = this.proxy(this.custom_events[key]);\n            this.on(key, this, method);\n        }\n    },\n    on(events, dest, func) {\n        var self = this;\n        if (typeof func !== \"function\") {\n            throw new Error(\"Event handler must be a function.\");\n        }\n        events = events.split(/\\s+/);\n        events.forEach((eventName) => {\n            self.__edispatcherEvents.on(eventName, func, dest);\n            if (dest && dest.__eventDispatcherMixin) {\n                dest.__edispatcherRegisteredEvents.push({name: eventName, func: func, source: self});\n            }\n        });\n        return this;\n    },\n    off(events, dest, func) {\n        var self = this;\n        events = events.split(/\\s+/);\n        events.forEach((eventName) => {\n            self.__edispatcherEvents.off(eventName, func, dest);\n            if (dest && dest.__eventDispatcherMixin) {\n                dest.__edispatcherRegisteredEvents = dest.__edispatcherRegisteredEvents.filter(el => {\n                    return !(el.name === eventName && el.func === func && el.source === self);\n                });\n            }\n        });\n        return this;\n    },\n    trigger() {\n        this.__edispatcherEvents.trigger.apply(this.__edispatcherEvents, arguments);\n        return this;\n    },\n    \"trigger_up\": function (name, info) {\n        var event = new OdooEvent(this, name, info);\n        //console.info('event: ', name, info);\n        this._trigger_up(event);\n        return event;\n    },\n    \"_trigger_up\": function (event) {\n        var parent;\n        this.__edispatcherEvents.trigger(event.name, event);\n        if (!event.is_stopped() && (parent = this.getParent())) {\n            parent._trigger_up(event);\n        }\n    },\n    destroy() {\n        var self = this;\n        this.__edispatcherRegisteredEvents.forEach((event) => {\n            event.source.__edispatcherEvents.off(event.name, event.func, self);\n        });\n        this.__edispatcherRegisteredEvents = [];\n        this.__edispatcherEvents.callbackList().forEach(\n            ((cal) => {\n                this.off(cal[0], cal[2], cal[1]);\n            }).bind(this)\n        );\n        this.__edispatcherEvents.off();\n        ParentedMixin.destroy.call(this);\n    },\n});\n\nfunction protectMethod(widget, fn) {\n    return function (...args) {\n        return new Promise((resolve, reject) => {\n            Promise.resolve(fn.call(this, ...args))\n                .then((result) => {\n                    if (!widget.isDestroyed()) {\n                        resolve(result);\n                    }\n                })\n                .catch((reason) => {\n                    if (!widget.isDestroyed()) {\n                        reject(reason);\n                    }\n                });\n        });\n    };\n}\n\nconst ServicesMixin = {\n    bindService: function (serviceName) {\n        const { services } = Component.env;\n        const service = services[serviceName];\n        if (!service) {\n            throw new Error(`Service ${serviceName} is not available`);\n        }\n        if (serviceName in SERVICES_METADATA) {\n            if (service instanceof Function) {\n                return protectMethod(this, service);\n            } else {\n                const methods = SERVICES_METADATA[serviceName];\n                const result = Object.create(service);\n                for (const method of methods) {\n                    result[method] = protectMethod(this, service[method]);\n                }\n                return result;\n            }\n        }\n        return service;\n    },\n    /**\n     * @param  {string} service\n     * @param  {string} method\n     * @return {any} result of the service called\n     */\n    call: function (service, method) {\n        var args = Array.prototype.slice.call(arguments, 2);\n        var result;\n        this.trigger_up('call_service', {\n            service: service,\n            method: method,\n            args: args,\n            callback: function (r) {\n                result = r;\n            },\n        });\n        return result;\n    },\n};\n\n/**\n * Base class for all visual components. Provides a lot of functions helpful\n * for the management of a part of the DOM.\n *\n * Widget handles:\n *\n * - Rendering with QWeb.\n * - Life-cycle management and parenting (when a parent is destroyed, all its\n *   children are destroyed too).\n * - Insertion in DOM.\n *\n * **Guide to create implementations of the Widget class**\n *\n * Here is a sample child class::\n *\n *     var MyWidget = Widget.extend({\n *         // the name of the QWeb template to use for rendering\n *         template: \"MyQWebTemplate\",\n *\n *         init: function (parent) {\n *             this._super(parent);\n *             // stuff that you want to init before the rendering\n *         },\n *         willStart: function () {\n *             // async work that need to be done before the widget is ready\n *             // this method should return a promise\n *         },\n *         start: function() {\n *             // stuff you want to make after the rendering, `this.$el` holds a correct value\n *             this.$(\".my_button\").click(/* an example of event binding * /);\n *\n *             // if you have some asynchronous operations, it's a good idea to return\n *             // a promise in start(). Note that this is quite rare, and if you\n *             // need to fetch some data, this should probably be done in the\n *             // willStart method\n *             var promise = this._rpc(...);\n *             return promise;\n *         }\n *     });\n *\n * Now this class can simply be used with the following syntax::\n *\n *     var myWidget = new MyWidget(this);\n *     myWidget.appendTo($(\".some-div\"));\n *\n * With these two lines, the MyWidget instance was initialized, rendered,\n * inserted into the DOM inside the ``.some-div`` div and its events were\n * bound.\n *\n * This class can also be initialized and started on an existing DOM element\n * using the `selector` property. See below for more documentation.\n *\n * And of course, when you don't need that widget anymore, just do::\n *\n *     myWidget.destroy();\n *\n * That will kill the widget in a clean way and erase its content from the dom.\n *\n * This class also provides a way for executing code once a website DOM element\n * is loaded in the dom.\n * @see PublicWidget.selector\n */\nexport const PublicWidget = Class.extend(EventDispatcherMixin, ServicesMixin, {\n    // Backbone-ish API\n    tagName: 'div',\n    id: null,\n    className: null,\n    attributes: {},\n    /**\n     * The name of the QWeb template that will be used for rendering. Must be\n     * redefined in subclasses or the default render() method can not be used.\n     *\n     * @type {null|string}\n     */\n    template: null,\n    /**\n     * List of paths to css files that need to be loaded before the widget can\n     * be rendered. This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    cssLibs: null,\n    /**\n     * List of paths to js files that need to be loaded before the widget can\n     * be rendered. This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    jsLibs: null,\n    /**\n     * List of xmlID that need to be loaded before the widget can be rendered.\n     * The content css (link file or style tag) and js (file or inline) of the\n     * assets are loaded.\n     * This will not induce loading anything that has already been\n     * loaded.\n     *\n     * @type {null|string[]}\n     */\n    assetLibs: null,\n    /**\n     * The selector attribute, if defined, allows to automatically create an\n     * instance of this widget on page load for each DOM element according to\n     * this selector. The `PublicWidget.$el / el` element will then be that\n     * particular DOM element. This should be the main way of instantiating\n     * `PublicWidget` elements.\n     *\n     * The value can either be a string in which case it is considered as a\n     * `querySelectorAll` selector to match, or a function expecting to return\n     * all DOM elements to consider, which are inside the element received as\n     * parameter of the function (or that element itself).\n     *\n     * @see selectorHas\n     *\n     * @todo do not make this part of the Widget but rather an info to give when\n     * registering the widget.\n     *\n     * @type {string|function|false}\n     */\n    selector: false,\n    /**\n     * The `selectorHas` attribute, if defined, allows to filter elements found\n     * through the `selector` attribute by only considering those which contain\n     * at least an element which matches this `selectorHas` selector.\n     *\n     * Note that this is the equivalent of setting up a `selector` using the\n     * `:has` pseudo-selector but that pseudo-selector is known to not be fully\n     * supported in all browsers. To prevent useless crashes, using this\n     * `selectorHas` attribute should be preferred.\n     *\n     * @type {string|false}\n     */\n    selectorHas: false,\n    /**\n     * Extension of @see Widget.events\n     *\n     * A description of the event handlers to bind/delegate once the widget\n     * has been rendered::\n     *\n     *   'click .hello .world': 'async _onHelloWorldClick',\n     *     _^_      _^_           _^_        _^_\n     *      |        |             |          |\n     *      |  (Optional) jQuery   |  Handler method name\n     *      |  delegate selector   |\n     *      |                      |_ (Optional) space separated options\n     *      |                          * async: use the automatic system\n     *      |_ Event name with           making handlers promise-ready (see\n     *         potential jQuery          makeButtonHandler, makeAsyncHandler)\n     *         namespaces\n     *\n     * Note: the values may be replaced by a function declaration. This is\n     * however a deprecated behavior.\n     *\n     * @type {Object}\n     */\n    events: {},\n\n    /**\n     * @constructor\n     * @param {Object} parent\n     * @param {Object} [options]\n     */\n    init: function (parent, options) {\n        EventDispatcherMixin.init.call(this);\n        this.setParent(parent);\n        this.options = options || {};\n    },\n    /**\n     * Method called between @see init and @see start. Performs asynchronous\n     * calls required by the rendering and the start method.\n     *\n     * This method should return a Promise which is resolved when start can be\n     * executed.\n     *\n     * @returns {Promise}\n     */\n    willStart: function () {\n        var proms = [];\n        if (this.jsLibs || this.cssLibs || this.assetLibs) {\n            var assetsPromise = Promise.all([\n                ...(this.cssLibs || []).map(loadCSS),\n                ...(this.jsLibs || []).map(loadJS),\n            ]);\n            for (const bundleName of this.assetLibs || []) {\n                if (typeof bundleName === \"string\") {\n                    assetsPromise = assetsPromise.then(() => {\n                        return loadBundle(bundleName);\n                    });\n                } else {\n                    assetsPromise = assetsPromise.then(() => {\n                        return Promise.all([...bundleName.map(loadBundle)]);\n                    });\n                }\n            }\n            proms.push(assetsPromise);\n        }\n        return Promise.all(proms);\n    },\n    /**\n     * Method called after rendering. Mostly used to bind actions, perform\n     * asynchronous calls, etc...\n     *\n     * By convention, this method should return an object that can be passed to\n     * Promise.resolve() to inform the caller when this widget has been initialized.\n     *\n     * Note that, for historic reasons, many widgets still do work in the start\n     * method that would be more suited to the willStart method.\n     *\n     * @returns {Promise}\n     */\n    start: function () {\n        return Promise.resolve();\n    },\n    /**\n     * Destroys the widget and basically restores the target to the state it\n     * was before the start method was called (unlike standard widget, the\n     * associated $el DOM is not removed, if this was instantiated thanks to the\n     * selector property).\n     */\n    destroy: function () {\n        EventDispatcherMixin.destroy.call(this);\n        if (this.$el) {\n            this._undelegateEvents();\n\n            // If not done with a selector (attached to existing DOM), then\n            // remove the elements added to the DOM.\n            if (!this.selector) {\n                this.$el.remove();\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Renders the current widget and appends it to the given jQuery object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    appendTo: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.appendTo(t);\n        }, target);\n    },\n    /**\n     * Attach the current widget to a dom element\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    attachTo: function (target) {\n        var self = this;\n        this.setElement(target.$el || target);\n        return this.willStart().then(function () {\n            if (self.__parentedDestroyed) {\n                return;\n            }\n            return self.start();\n        });\n    },\n    /**\n     * Renders the current widget and inserts it after to the given jQuery\n     * object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    insertAfter: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.insertAfter(t);\n        }, target);\n    },\n    /**\n     * Renders the current widget and inserts it before to the given jQuery\n     * object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    insertBefore: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.insertBefore(t);\n        }, target);\n    },\n    /**\n     * Renders the current widget and prepends it to the given jQuery object.\n     *\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    prependTo: function (target) {\n        var self = this;\n        return this._widgetRenderAndInsert(function (t) {\n            self.$el.prependTo(t);\n        }, target);\n    },\n    /**\n     * Renders the element. The default implementation renders the widget using\n     * QWeb, `this.template` must be defined. The context given to QWeb contains\n     * the \"widget\" key that references `this`.\n     */\n    renderElement: function () {\n        var $el;\n        if (this.template) {\n            $el = $(renderToElement(this.template, {widget: this}));\n        } else {\n            $el = this._makeDescriptive();\n        }\n        this._replaceElement($el);\n    },\n    /**\n     * Renders the current widget and replaces the given jQuery object.\n     *\n     * @param target A jQuery object or a Widget instance.\n     * @returns {Promise}\n     */\n    replace: function (target) {\n        return this._widgetRenderAndInsert((t) => {\n            this.$el.replaceAll(t);\n        }, target);\n    },\n    /**\n     * Re-sets the widget's root element (el/$el/$el).\n     *\n     * Includes:\n     *\n     * * re-delegating events\n     * * re-binding sub-elements\n     * * if the widget already had a root element, replacing the pre-existing\n     *   element in the DOM\n     *\n     * @param {HTMLElement | jQuery} element new root element for the widget\n     * @return {Widget} this\n     */\n    setElement: function (element) {\n        if (this.$el) {\n            this._undelegateEvents();\n        }\n\n        this.$el = (element instanceof $) ? element : $(element);\n        this.el = this.$el[0];\n\n        this._delegateEvents();\n\n        if (this.selector) {\n            this.$target = this.$el;\n            this.target = this.el;\n        }\n\n        return this;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Helper method, for ``this.$el.find(selector)``\n     *\n     * @private\n     * @param {string} selector CSS selector, rooted in $el\n     * @returns {jQuery} selector match\n     */\n    $: function (selector) {\n        if (selector === undefined) {\n            return this.$el;\n        }\n        return this.$el.find(selector);\n    },\n    /**\n     * @see this.events\n     * @override\n     */\n    _delegateEvents: function () {\n        var self = this;\n\n        const _delegateEvent = (method, key) => {\n            var match = /^(\\S+)(\\s+(.*))?$/.exec(key);\n            var event = match[1];\n            var selector = match[3];\n\n            event += '.widget_events';\n            if (!selector) {\n                self.$el.on(event, method);\n            } else {\n                self.$el.on(event, selector, method);\n            }\n        };\n        Object.entries(this.events || {}).forEach(([event, method]) => {\n            // If the method is a function, use the default Widget system\n            if (typeof method !== 'string') {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n            // If the method is only a function name without options, use the\n            // default Widget system\n            var methodOptions = method.split(' ');\n            if (methodOptions.length <= 1) {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n            // If the method has no meaningful options, use the default Widget\n            // system\n            var isAsync = methodOptions.includes('async');\n            if (!isAsync) {\n                _delegateEvent(self.proxy(method), event);\n                return;\n            }\n\n            method = self.proxy(methodOptions[methodOptions.length - 1]);\n            if (String(event).startsWith(\"click\")) {\n                // Protect click handler to be called multiple times by\n                // mistake by the user and add a visual disabling effect\n                // for buttons.\n                method = makeButtonHandler(method);\n            } else {\n                // Protect all handlers to be recalled while the previous\n                // async handler call is not finished.\n                method = makeAsyncHandler(method);\n            }\n            _delegateEvent(method, event);\n        });\n    },\n    /**\n     * @private\n     * @param {boolean} [extra=false]\n     * @param {Object} [extraContext]\n     * @returns {Object}\n     */\n    _getContext: function (extra, extraContext) {\n        var context;\n        this.trigger_up('context_get', {\n            extra: extra || false,\n            context: extraContext,\n            callback: function (ctx) {\n                context = ctx;\n            },\n        });\n        return context;\n    },\n    /**\n     * Makes a potential root element from the declarative builder of the\n     * widget\n     *\n     * @private\n     * @return {jQuery}\n     */\n    _makeDescriptive: function () {\n        var attrs = Object.assign({}, this.attributes || {});\n        if (this.id) {\n            attrs.id = this.id;\n        }\n        if (this.className) {\n            attrs['class'] = this.className;\n        }\n        var $el = $(document.createElement(this.tagName));\n        if (Object.keys(attrs || {}).length > 0) {\n            $el.attr(attrs);\n        }\n        return $el;\n    },\n    /**\n     * Re-sets the widget's root element and replaces the old root element\n     * (if any) by the new one in the DOM.\n     *\n     * @private\n     * @param {HTMLElement | jQuery} $el\n     * @returns {Widget} this instance, so it can be chained\n     */\n    _replaceElement: function ($el) {\n        var $oldel = this.$el;\n        this.setElement($el);\n        if ($oldel && !$oldel.is(this.$el)) {\n            if ($oldel.length > 1) {\n                $oldel.wrapAll('<div/>');\n                $oldel.parent().replaceWith(this.$el);\n            } else {\n                $oldel.replaceWith(this.$el);\n            }\n        }\n        return this;\n    },\n    /**\n     * Remove all handlers registered on this.$el\n     *\n     * @private\n     */\n    _undelegateEvents: function () {\n        this.$el.off('.widget_events');\n    },\n    /**\n     * Render the widget.  This is a private method, and should really never be\n     * called by anyone (except this widget).  It assumes that the widget was\n     * not willStarted yet.\n     *\n     * @private\n     * @param {function: jQuery -> any} insertion\n     * @param {jQuery} target\n     * @returns {Promise}\n     */\n    _widgetRenderAndInsert: function (insertion, target) {\n        var self = this;\n        return this.willStart().then(function () {\n            if (self.__parentedDestroyed) {\n                return;\n            }\n            self.renderElement();\n            insertion(target);\n            return self.start();\n        });\n    },\n});\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\n/**\n * The registry object contains the list of widgets that should be instantiated\n * thanks to their selector property if any.\n */\nvar registry = {};\n\nexport default {\n    Widget: PublicWidget,\n    registry: registry,\n\n    ParentedMixin: ParentedMixin,\n    EventDispatcherMixin: EventDispatcherMixin,\n    ServicesMixin: ServicesMixin,\n};\n", "/*!\n * Cropper.js v1.5.5\n * https://fengyuanchen.github.io/cropperjs\n *\n * Copyright 2015-present Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2019-08-04T02:26:31.160Z\n */\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 = global || self, global.Cropper = factory());\n}(this, function () { 'use strict';\n\n  function _typeof(obj) {\n    if (typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\") {\n      _typeof = function (obj) {\n        return typeof obj;\n      };\n    } else {\n      _typeof = function (obj) {\n        return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n      };\n    }\n\n    return _typeof(obj);\n  }\n\n  function _classCallCheck(instance, Constructor) {\n    if (!(instance instanceof Constructor)) {\n      throw new TypeError(\"Cannot call a class as a function\");\n    }\n  }\n\n  function _defineProperties(target, props) {\n    for (var i = 0; i < props.length; i++) {\n      var descriptor = props[i];\n      descriptor.enumerable = descriptor.enumerable || false;\n      descriptor.configurable = true;\n      if (\"value\" in descriptor) descriptor.writable = true;\n      Object.defineProperty(target, descriptor.key, descriptor);\n    }\n  }\n\n  function _createClass(Constructor, protoProps, staticProps) {\n    if (protoProps) _defineProperties(Constructor.prototype, protoProps);\n    if (staticProps) _defineProperties(Constructor, staticProps);\n    return Constructor;\n  }\n\n  function _toConsumableArray(arr) {\n    return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread();\n  }\n\n  function _arrayWithoutHoles(arr) {\n    if (Array.isArray(arr)) {\n      for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];\n\n      return arr2;\n    }\n  }\n\n  function _iterableToArray(iter) {\n    if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === \"[object Arguments]\") return Array.from(iter);\n  }\n\n  function _nonIterableSpread() {\n    throw new TypeError(\"Invalid attempt to spread non-iterable instance\");\n  }\n\n  var IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined';\n  var WINDOW = IS_BROWSER ? window : {};\n  var IS_TOUCH_DEVICE = IS_BROWSER ? 'ontouchstart' in WINDOW.document.documentElement : false;\n  var HAS_POINTER_EVENT = IS_BROWSER ? 'PointerEvent' in WINDOW : false;\n  var NAMESPACE = 'cropper'; // Actions\n\n  var ACTION_ALL = 'all';\n  var ACTION_CROP = 'crop';\n  var ACTION_MOVE = 'move';\n  var ACTION_ZOOM = 'zoom';\n  var ACTION_EAST = 'e';\n  var ACTION_WEST = 'w';\n  var ACTION_SOUTH = 's';\n  var ACTION_NORTH = 'n';\n  var ACTION_NORTH_EAST = 'ne';\n  var ACTION_NORTH_WEST = 'nw';\n  var ACTION_SOUTH_EAST = 'se';\n  var ACTION_SOUTH_WEST = 'sw'; // Classes\n\n  var CLASS_CROP = \"\".concat(NAMESPACE, \"-crop\");\n  var CLASS_DISABLED = \"\".concat(NAMESPACE, \"-disabled\");\n  var CLASS_HIDDEN = \"\".concat(NAMESPACE, \"-hidden\");\n  var CLASS_HIDE = \"\".concat(NAMESPACE, \"-hide\");\n  var CLASS_INVISIBLE = \"\".concat(NAMESPACE, \"-invisible\");\n  var CLASS_MODAL = \"\".concat(NAMESPACE, \"-modal\");\n  var CLASS_MOVE = \"\".concat(NAMESPACE, \"-move\"); // Data keys\n\n  var DATA_ACTION = \"\".concat(NAMESPACE, \"Action\");\n  var DATA_PREVIEW = \"\".concat(NAMESPACE, \"Preview\"); // Drag modes\n\n  var DRAG_MODE_CROP = 'crop';\n  var DRAG_MODE_MOVE = 'move';\n  var DRAG_MODE_NONE = 'none'; // Events\n\n  var EVENT_CROP = 'crop';\n  var EVENT_CROP_END = 'cropend';\n  var EVENT_CROP_MOVE = 'cropmove';\n  var EVENT_CROP_START = 'cropstart';\n  var EVENT_DBLCLICK = 'dblclick';\n  var EVENT_TOUCH_START = IS_TOUCH_DEVICE ? 'touchstart' : 'mousedown';\n  var EVENT_TOUCH_MOVE = IS_TOUCH_DEVICE ? 'touchmove' : 'mousemove';\n  var EVENT_TOUCH_END = IS_TOUCH_DEVICE ? 'touchend touchcancel' : 'mouseup';\n  var EVENT_POINTER_DOWN = HAS_POINTER_EVENT ? 'pointerdown' : EVENT_TOUCH_START;\n  var EVENT_POINTER_MOVE = HAS_POINTER_EVENT ? 'pointermove' : EVENT_TOUCH_MOVE;\n  var EVENT_POINTER_UP = HAS_POINTER_EVENT ? 'pointerup pointercancel' : EVENT_TOUCH_END;\n  var EVENT_READY = 'ready';\n  var EVENT_RESIZE = 'resize';\n  var EVENT_WHEEL = 'wheel';\n  var EVENT_ZOOM = 'zoom'; // Mime types\n\n  var MIME_TYPE_JPEG = 'image/jpeg'; // RegExps\n\n  var REGEXP_ACTIONS = /^e|w|s|n|se|sw|ne|nw|all|crop|move|zoom$/;\n  var REGEXP_DATA_URL = /^data:/;\n  var REGEXP_DATA_URL_JPEG = /^data:image\\/jpeg;base64,/;\n  var REGEXP_TAG_NAME = /^img|canvas$/i; // Misc\n  // Inspired by the default width and height of a canvas element.\n\n  var MIN_CONTAINER_WIDTH = 200;\n  var MIN_CONTAINER_HEIGHT = 100;\n\n  var DEFAULTS = {\n    // Define the view mode of the cropper\n    viewMode: 0,\n    // 0, 1, 2, 3\n    // Define the dragging mode of the cropper\n    dragMode: DRAG_MODE_CROP,\n    // 'crop', 'move' or 'none'\n    // Define the initial aspect ratio of the crop box\n    initialAspectRatio: NaN,\n    // Define the aspect ratio of the crop box\n    aspectRatio: NaN,\n    // An object with the previous cropping result data\n    data: null,\n    // A selector for adding extra containers to preview\n    preview: '',\n    // Re-render the cropper when resize the window\n    responsive: true,\n    // Restore the cropped area after resize the window\n    restore: true,\n    // Check if the current image is a cross-origin image\n    checkCrossOrigin: true,\n    // Check the current image's Exif Orientation information\n    checkOrientation: true,\n    // Show the black modal\n    modal: true,\n    // Show the dashed lines for guiding\n    guides: true,\n    // Show the center indicator for guiding\n    center: true,\n    // Show the white modal to highlight the crop box\n    highlight: true,\n    // Show the grid background\n    background: true,\n    // Enable to crop the image automatically when initialize\n    autoCrop: true,\n    // Define the percentage of automatic cropping area when initializes\n    autoCropArea: 0.8,\n    // Enable to move the image\n    movable: true,\n    // Enable to rotate the image\n    rotatable: true,\n    // Enable to scale the image\n    scalable: true,\n    // Enable to zoom the image\n    zoomable: true,\n    // Enable to zoom the image by dragging touch\n    zoomOnTouch: true,\n    // Enable to zoom the image by wheeling mouse\n    zoomOnWheel: true,\n    // Define zoom ratio when zoom the image by wheeling mouse\n    wheelZoomRatio: 0.1,\n    // Enable to move the crop box\n    cropBoxMovable: true,\n    // Enable to resize the crop box\n    cropBoxResizable: true,\n    // Toggle drag mode between \"crop\" and \"move\" when click twice on the cropper\n    toggleDragModeOnDblclick: true,\n    // Size limitation\n    minCanvasWidth: 0,\n    minCanvasHeight: 0,\n    minCropBoxWidth: 0,\n    minCropBoxHeight: 0,\n    minContainerWidth: 200,\n    minContainerHeight: 100,\n    // Shortcuts of events\n    ready: null,\n    cropstart: null,\n    cropmove: null,\n    cropend: null,\n    crop: null,\n    zoom: null\n  };\n\n  var TEMPLATE = '<div class=\"cropper-container\" touch-action=\"none\">' + '<div class=\"cropper-wrap-box\">' + '<div class=\"cropper-canvas\"></div>' + '</div>' + '<div class=\"cropper-drag-box\"></div>' + '<div class=\"cropper-crop-box\">' + '<span class=\"cropper-view-box\"></span>' + '<span class=\"cropper-dashed dashed-h\"></span>' + '<span class=\"cropper-dashed dashed-v\"></span>' + '<span class=\"cropper-center\"></span>' + '<span class=\"cropper-face\"></span>' + '<span class=\"cropper-line line-e\" data-cropper-action=\"e\"></span>' + '<span class=\"cropper-line line-n\" data-cropper-action=\"n\"></span>' + '<span class=\"cropper-line line-w\" data-cropper-action=\"w\"></span>' + '<span class=\"cropper-line line-s\" data-cropper-action=\"s\"></span>' + '<span class=\"cropper-point point-e\" data-cropper-action=\"e\"></span>' + '<span class=\"cropper-point point-n\" data-cropper-action=\"n\"></span>' + '<span class=\"cropper-point point-w\" data-cropper-action=\"w\"></span>' + '<span class=\"cropper-point point-s\" data-cropper-action=\"s\"></span>' + '<span class=\"cropper-point point-ne\" data-cropper-action=\"ne\"></span>' + '<span class=\"cropper-point point-nw\" data-cropper-action=\"nw\"></span>' + '<span class=\"cropper-point point-sw\" data-cropper-action=\"sw\"></span>' + '<span class=\"cropper-point point-se\" data-cropper-action=\"se\"></span>' + '</div>' + '</div>';\n\n  /**\n   * Check if the given value is not a number.\n   */\n\n  var isNaN = Number.isNaN || WINDOW.isNaN;\n  /**\n   * Check if the given value is a number.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is a number, else `false`.\n   */\n\n  function isNumber(value) {\n    return typeof value === 'number' && !isNaN(value);\n  }\n  /**\n   * Check if the given value is a positive number.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is a positive number, else `false`.\n   */\n\n  var isPositiveNumber = function isPositiveNumber(value) {\n    return value > 0 && value < Infinity;\n  };\n  /**\n   * Check if the given value is undefined.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is undefined, else `false`.\n   */\n\n  function isUndefined(value) {\n    return typeof value === 'undefined';\n  }\n  /**\n   * Check if the given value is an object.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is an object, else `false`.\n   */\n\n  function isObject(value) {\n    return _typeof(value) === 'object' && value !== null;\n  }\n  var hasOwnProperty = Object.prototype.hasOwnProperty;\n  /**\n   * Check if the given value is a plain object.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is a plain object, else `false`.\n   */\n\n  function isPlainObject(value) {\n    if (!isObject(value)) {\n      return false;\n    }\n\n    try {\n      var _constructor = value.constructor;\n      var prototype = _constructor.prototype;\n      return _constructor && prototype && hasOwnProperty.call(prototype, 'isPrototypeOf');\n    } catch (error) {\n      return false;\n    }\n  }\n  /**\n   * Check if the given value is a function.\n   * @param {*} value - The value to check.\n   * @returns {boolean} Returns `true` if the given value is a function, else `false`.\n   */\n\n  function isFunction(value) {\n    return typeof value === 'function';\n  }\n  var slice = Array.prototype.slice;\n  /**\n   * Convert array-like or iterable object to an array.\n   * @param {*} value - The value to convert.\n   * @returns {Array} Returns a new array.\n   */\n\n  function toArray(value) {\n    return Array.from ? Array.from(value) : slice.call(value);\n  }\n  /**\n   * Iterate the given data.\n   * @param {*} data - The data to iterate.\n   * @param {Function} callback - The process function for each element.\n   * @returns {*} The original data.\n   */\n\n  function forEach(data, callback) {\n    if (data && isFunction(callback)) {\n      if (Array.isArray(data) || isNumber(data.length)\n      /* array-like */\n      ) {\n          toArray(data).forEach(function (value, key) {\n            callback.call(data, value, key, data);\n          });\n        } else if (isObject(data)) {\n        Object.keys(data).forEach(function (key) {\n          callback.call(data, data[key], key, data);\n        });\n      }\n    }\n\n    return data;\n  }\n  /**\n   * Extend the given object.\n   * @param {*} target - The target object to extend.\n   * @param {*} args - The rest objects for merging to the target object.\n   * @returns {Object} The extended object.\n   */\n\n  var assign = Object.assign || function assign(target) {\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\n    if (isObject(target) && args.length > 0) {\n      args.forEach(function (arg) {\n        if (isObject(arg)) {\n          Object.keys(arg).forEach(function (key) {\n            target[key] = arg[key];\n          });\n        }\n      });\n    }\n\n    return target;\n  };\n  var REGEXP_DECIMALS = /\\.\\d*(?:0|9){12}\\d*$/;\n  /**\n   * Normalize decimal number.\n   * Check out {@link http://0.30000000000000004.com/}\n   * @param {number} value - The value to normalize.\n   * @param {number} [times=100000000000] - The times for normalizing.\n   * @returns {number} Returns the normalized number.\n   */\n\n  function normalizeDecimalNumber(value) {\n    var times = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 100000000000;\n    return REGEXP_DECIMALS.test(value) ? Math.round(value * times) / times : value;\n  }\n  var REGEXP_SUFFIX = /^width|height|left|top|marginLeft|marginTop$/;\n  /**\n   * Apply styles to the given element.\n   * @param {Element} element - The target element.\n   * @param {Object} styles - The styles for applying.\n   */\n\n  function setStyle(element, styles) {\n    var style = element.style;\n    forEach(styles, function (value, property) {\n      if (REGEXP_SUFFIX.test(property) && isNumber(value)) {\n        value = \"\".concat(value, \"px\");\n      }\n\n      style[property] = value;\n    });\n  }\n  /**\n   * Check if the given element has a special class.\n   * @param {Element} element - The element to check.\n   * @param {string} value - The class to search.\n   * @returns {boolean} Returns `true` if the special class was found.\n   */\n\n  function hasClass(element, value) {\n    return element.classList ? element.classList.contains(value) : element.className.indexOf(value) > -1;\n  }\n  /**\n   * Add classes to the given element.\n   * @param {Element} element - The target element.\n   * @param {string} value - The classes to be added.\n   */\n\n  function addClass(element, value) {\n    if (!value) {\n      return;\n    }\n\n    if (isNumber(element.length)) {\n      forEach(element, function (elem) {\n        addClass(elem, value);\n      });\n      return;\n    }\n\n    if (element.classList) {\n      element.classList.add(value);\n      return;\n    }\n\n    var className = element.className.trim();\n\n    if (!className) {\n      element.className = value;\n    } else if (className.indexOf(value) < 0) {\n      element.className = \"\".concat(className, \" \").concat(value);\n    }\n  }\n  /**\n   * Remove classes from the given element.\n   * @param {Element} element - The target element.\n   * @param {string} value - The classes to be removed.\n   */\n\n  function removeClass(element, value) {\n    if (!value) {\n      return;\n    }\n\n    if (isNumber(element.length)) {\n      forEach(element, function (elem) {\n        removeClass(elem, value);\n      });\n      return;\n    }\n\n    if (element.classList) {\n      element.classList.remove(value);\n      return;\n    }\n\n    if (element.className.indexOf(value) >= 0) {\n      element.className = element.className.replace(value, '');\n    }\n  }\n  /**\n   * Add or remove classes from the given element.\n   * @param {Element} element - The target element.\n   * @param {string} value - The classes to be toggled.\n   * @param {boolean} added - Add only.\n   */\n\n  function toggleClass(element, value, added) {\n    if (!value) {\n      return;\n    }\n\n    if (isNumber(element.length)) {\n      forEach(element, function (elem) {\n        toggleClass(elem, value, added);\n      });\n      return;\n    } // IE10-11 doesn't support the second parameter of `classList.toggle`\n\n\n    if (added) {\n      addClass(element, value);\n    } else {\n      removeClass(element, value);\n    }\n  }\n  var REGEXP_CAMEL_CASE = /([a-z\\d])([A-Z])/g;\n  /**\n   * Transform the given string from camelCase to kebab-case\n   * @param {string} value - The value to transform.\n   * @returns {string} The transformed value.\n   */\n\n  function toParamCase(value) {\n    return value.replace(REGEXP_CAMEL_CASE, '$1-$2').toLowerCase();\n  }\n  /**\n   * Get data from the given element.\n   * @param {Element} element - The target element.\n   * @param {string} name - The data key to get.\n   * @returns {string} The data value.\n   */\n\n  function getData(element, name) {\n    if (isObject(element[name])) {\n      return element[name];\n    }\n\n    if (element.dataset) {\n      return element.dataset[name];\n    }\n\n    return element.getAttribute(\"data-\".concat(toParamCase(name)));\n  }\n  /**\n   * Set data to the given element.\n   * @param {Element} element - The target element.\n   * @param {string} name - The data key to set.\n   * @param {string} data - The data value.\n   */\n\n  function setData(element, name, data) {\n    if (isObject(data)) {\n      element[name] = data;\n    } else if (element.dataset) {\n      element.dataset[name] = data;\n    } else {\n      element.setAttribute(\"data-\".concat(toParamCase(name)), data);\n    }\n  }\n  /**\n   * Remove data from the given element.\n   * @param {Element} element - The target element.\n   * @param {string} name - The data key to remove.\n   */\n\n  function removeData(element, name) {\n    if (isObject(element[name])) {\n      try {\n        delete element[name];\n      } catch (error) {\n        element[name] = undefined;\n      }\n    } else if (element.dataset) {\n      // #128 Safari not allows to delete dataset property\n      try {\n        delete element.dataset[name];\n      } catch (error) {\n        element.dataset[name] = undefined;\n      }\n    } else {\n      element.removeAttribute(\"data-\".concat(toParamCase(name)));\n    }\n  }\n  var REGEXP_SPACES = /\\s\\s*/;\n\n  var onceSupported = function () {\n    var supported = false;\n\n    if (IS_BROWSER) {\n      var once = false;\n\n      var listener = function listener() {};\n\n      var options = Object.defineProperty({}, 'once', {\n        get: function get() {\n          supported = true;\n          return once;\n        },\n\n        /**\n         * This setter can fix a `TypeError` in strict mode\n         * {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Getter_only}\n         * @param {boolean} value - The value to set\n         */\n        set: function set(value) {\n          once = value;\n        }\n      });\n      WINDOW.addEventListener('test', listener, options);\n      WINDOW.removeEventListener('test', listener, options);\n    }\n\n    return supported;\n  }();\n  /**\n   * Remove event listener from the target element.\n   * @param {Element} element - The event target.\n   * @param {string} type - The event type(s).\n   * @param {Function} listener - The event listener.\n   * @param {Object} options - The event options.\n   */\n\n\n  function removeListener(element, type, listener) {\n    var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};\n    var handler = listener;\n    type.trim().split(REGEXP_SPACES).forEach(function (event) {\n      if (!onceSupported) {\n        var listeners = element.listeners;\n\n        if (listeners && listeners[event] && listeners[event][listener]) {\n          handler = listeners[event][listener];\n          delete listeners[event][listener];\n\n          if (Object.keys(listeners[event]).length === 0) {\n            delete listeners[event];\n          }\n\n          if (Object.keys(listeners).length === 0) {\n            delete element.listeners;\n          }\n        }\n      }\n\n      element.removeEventListener(event, handler, options);\n    });\n  }\n  /**\n   * Add event listener to the target element.\n   * @param {Element} element - The event target.\n   * @param {string} type - The event type(s).\n   * @param {Function} listener - The event listener.\n   * @param {Object} options - The event options.\n   */\n\n  function addListener(element, type, listener) {\n    var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};\n    var _handler = listener;\n    type.trim().split(REGEXP_SPACES).forEach(function (event) {\n      if (options.once && !onceSupported) {\n        var _element$listeners = element.listeners,\n            listeners = _element$listeners === void 0 ? {} : _element$listeners;\n\n        _handler = function handler() {\n          delete listeners[event][listener];\n          element.removeEventListener(event, _handler, options);\n\n          for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n            args[_key2] = arguments[_key2];\n          }\n\n          listener.apply(element, args);\n        };\n\n        if (!listeners[event]) {\n          listeners[event] = {};\n        }\n\n        if (listeners[event][listener]) {\n          element.removeEventListener(event, listeners[event][listener], options);\n        }\n\n        listeners[event][listener] = _handler;\n        element.listeners = listeners;\n      }\n\n      element.addEventListener(event, _handler, options);\n    });\n  }\n  /**\n   * Dispatch event on the target element.\n   * @param {Element} element - The event target.\n   * @param {string} type - The event type(s).\n   * @param {Object} data - The additional event data.\n   * @returns {boolean} Indicate if the event is default prevented or not.\n   */\n\n  function dispatchEvent(element, type, data) {\n    var event; // Event and CustomEvent on IE9-11 are global objects, not constructors\n\n    if (isFunction(Event) && isFunction(CustomEvent)) {\n      event = new CustomEvent(type, {\n        detail: data,\n        bubbles: true,\n        cancelable: true\n      });\n    } else {\n      event = document.createEvent('CustomEvent');\n      event.initCustomEvent(type, true, true, data);\n    }\n\n    return element.dispatchEvent(event);\n  }\n  /**\n   * Get the offset base on the document.\n   * @param {Element} element - The target element.\n   * @returns {Object} The offset data.\n   */\n\n  function getOffset(element) {\n    var box = element.getBoundingClientRect();\n    return {\n      left: box.left + (window.pageXOffset - document.documentElement.clientLeft),\n      top: box.top + (window.pageYOffset - document.documentElement.clientTop)\n    };\n  }\n  var location = WINDOW.location;\n  var REGEXP_ORIGINS = /^(\\w+:)\\/\\/([^:/?#]*):?(\\d*)/i;\n  /**\n   * Check if the given URL is a cross origin URL.\n   * @param {string} url - The target URL.\n   * @returns {boolean} Returns `true` if the given URL is a cross origin URL, else `false`.\n   */\n\n  function isCrossOriginURL(url) {\n    var parts = url.match(REGEXP_ORIGINS);\n    return parts !== null && (parts[1] !== location.protocol || parts[2] !== location.hostname || parts[3] !== location.port);\n  }\n  /**\n   * Add timestamp to the given URL.\n   * @param {string} url - The target URL.\n   * @returns {string} The result URL.\n   */\n\n  function addTimestamp(url) {\n    var timestamp = \"timestamp=\".concat(new Date().getTime());\n    return url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp;\n  }\n  /**\n   * Get transforms base on the given object.\n   * @param {Object} obj - The target object.\n   * @returns {string} A string contains transform values.\n   */\n\n  function getTransforms(_ref) {\n    var rotate = _ref.rotate,\n        scaleX = _ref.scaleX,\n        scaleY = _ref.scaleY,\n        translateX = _ref.translateX,\n        translateY = _ref.translateY;\n    var values = [];\n\n    if (isNumber(translateX) && translateX !== 0) {\n      values.push(\"translateX(\".concat(translateX, \"px)\"));\n    }\n\n    if (isNumber(translateY) && translateY !== 0) {\n      values.push(\"translateY(\".concat(translateY, \"px)\"));\n    } // Rotate should come first before scale to match orientation transform\n\n\n    if (isNumber(rotate) && rotate !== 0) {\n      values.push(\"rotate(\".concat(rotate, \"deg)\"));\n    }\n\n    if (isNumber(scaleX) && scaleX !== 1) {\n      values.push(\"scaleX(\".concat(scaleX, \")\"));\n    }\n\n    if (isNumber(scaleY) && scaleY !== 1) {\n      values.push(\"scaleY(\".concat(scaleY, \")\"));\n    }\n\n    var transform = values.length ? values.join(' ') : 'none';\n    return {\n      WebkitTransform: transform,\n      msTransform: transform,\n      transform: transform\n    };\n  }\n  /**\n   * Get the max ratio of a group of pointers.\n   * @param {string} pointers - The target pointers.\n   * @returns {number} The result ratio.\n   */\n\n  function getMaxZoomRatio(pointers) {\n    var pointers2 = assign({}, pointers);\n    var ratios = [];\n    forEach(pointers, function (pointer, pointerId) {\n      delete pointers2[pointerId];\n      forEach(pointers2, function (pointer2) {\n        var x1 = Math.abs(pointer.startX - pointer2.startX);\n        var y1 = Math.abs(pointer.startY - pointer2.startY);\n        var x2 = Math.abs(pointer.endX - pointer2.endX);\n        var y2 = Math.abs(pointer.endY - pointer2.endY);\n        var z1 = Math.sqrt(x1 * x1 + y1 * y1);\n        var z2 = Math.sqrt(x2 * x2 + y2 * y2);\n        var ratio = (z2 - z1) / z1;\n        ratios.push(ratio);\n      });\n    });\n    ratios.sort(function (a, b) {\n      return Math.abs(a) < Math.abs(b);\n    });\n    return ratios[0];\n  }\n  /**\n   * Get a pointer from an event object.\n   * @param {Object} event - The target event object.\n   * @param {boolean} endOnly - Indicates if only returns the end point coordinate or not.\n   * @returns {Object} The result pointer contains start and/or end point coordinates.\n   */\n\n  function getPointer(_ref2, endOnly) {\n    var pageX = _ref2.pageX,\n        pageY = _ref2.pageY;\n    var end = {\n      endX: pageX,\n      endY: pageY\n    };\n    return endOnly ? end : assign({\n      startX: pageX,\n      startY: pageY\n    }, end);\n  }\n  /**\n   * Get the center point coordinate of a group of pointers.\n   * @param {Object} pointers - The target pointers.\n   * @returns {Object} The center point coordinate.\n   */\n\n  function getPointersCenter(pointers) {\n    var pageX = 0;\n    var pageY = 0;\n    var count = 0;\n    forEach(pointers, function (_ref3) {\n      var startX = _ref3.startX,\n          startY = _ref3.startY;\n      pageX += startX;\n      pageY += startY;\n      count += 1;\n    });\n    pageX /= count;\n    pageY /= count;\n    return {\n      pageX: pageX,\n      pageY: pageY\n    };\n  }\n  /**\n   * Get the max sizes in a rectangle under the given aspect ratio.\n   * @param {Object} data - The original sizes.\n   * @param {string} [type='contain'] - The adjust type.\n   * @returns {Object} The result sizes.\n   */\n\n  function getAdjustedSizes(_ref4) // or 'cover'\n  {\n    var aspectRatio = _ref4.aspectRatio,\n        height = _ref4.height,\n        width = _ref4.width;\n    var type = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'contain';\n    var isValidWidth = isPositiveNumber(width);\n    var isValidHeight = isPositiveNumber(height);\n\n    if (isValidWidth && isValidHeight) {\n      var adjustedWidth = height * aspectRatio;\n\n      if (type === 'contain' && adjustedWidth > width || type === 'cover' && adjustedWidth < width) {\n        height = width / aspectRatio;\n      } else {\n        width = height * aspectRatio;\n      }\n    } else if (isValidWidth) {\n      height = width / aspectRatio;\n    } else if (isValidHeight) {\n      width = height * aspectRatio;\n    }\n\n    return {\n      width: width,\n      height: height\n    };\n  }\n  /**\n   * Get the new sizes of a rectangle after rotated.\n   * @param {Object} data - The original sizes.\n   * @returns {Object} The result sizes.\n   */\n\n  function getRotatedSizes(_ref5) {\n    var width = _ref5.width,\n        height = _ref5.height,\n        degree = _ref5.degree;\n    degree = Math.abs(degree) % 180;\n\n    if (degree === 90) {\n      return {\n        width: height,\n        height: width\n      };\n    }\n\n    var arc = degree % 90 * Math.PI / 180;\n    var sinArc = Math.sin(arc);\n    var cosArc = Math.cos(arc);\n    var newWidth = width * cosArc + height * sinArc;\n    var newHeight = width * sinArc + height * cosArc;\n    return degree > 90 ? {\n      width: newHeight,\n      height: newWidth\n    } : {\n      width: newWidth,\n      height: newHeight\n    };\n  }\n  /**\n   * Get a canvas which drew the given image.\n   * @param {HTMLImageElement} image - The image for drawing.\n   * @param {Object} imageData - The image data.\n   * @param {Object} canvasData - The canvas data.\n   * @param {Object} options - The options.\n   * @returns {HTMLCanvasElement} The result canvas.\n   */\n\n  function getSourceCanvas(image, _ref6, _ref7, _ref8) {\n    var imageAspectRatio = _ref6.aspectRatio,\n        imageNaturalWidth = _ref6.naturalWidth,\n        imageNaturalHeight = _ref6.naturalHeight,\n        _ref6$rotate = _ref6.rotate,\n        rotate = _ref6$rotate === void 0 ? 0 : _ref6$rotate,\n        _ref6$scaleX = _ref6.scaleX,\n        scaleX = _ref6$scaleX === void 0 ? 1 : _ref6$scaleX,\n        _ref6$scaleY = _ref6.scaleY,\n        scaleY = _ref6$scaleY === void 0 ? 1 : _ref6$scaleY;\n    var aspectRatio = _ref7.aspectRatio,\n        naturalWidth = _ref7.naturalWidth,\n        naturalHeight = _ref7.naturalHeight;\n    var _ref8$fillColor = _ref8.fillColor,\n        fillColor = _ref8$fillColor === void 0 ? 'transparent' : _ref8$fillColor,\n        _ref8$imageSmoothingE = _ref8.imageSmoothingEnabled,\n        imageSmoothingEnabled = _ref8$imageSmoothingE === void 0 ? true : _ref8$imageSmoothingE,\n        _ref8$imageSmoothingQ = _ref8.imageSmoothingQuality,\n        imageSmoothingQuality = _ref8$imageSmoothingQ === void 0 ? 'low' : _ref8$imageSmoothingQ,\n        _ref8$maxWidth = _ref8.maxWidth,\n        maxWidth = _ref8$maxWidth === void 0 ? Infinity : _ref8$maxWidth,\n        _ref8$maxHeight = _ref8.maxHeight,\n        maxHeight = _ref8$maxHeight === void 0 ? Infinity : _ref8$maxHeight,\n        _ref8$minWidth = _ref8.minWidth,\n        minWidth = _ref8$minWidth === void 0 ? 0 : _ref8$minWidth,\n        _ref8$minHeight = _ref8.minHeight,\n        minHeight = _ref8$minHeight === void 0 ? 0 : _ref8$minHeight;\n    var canvas = document.createElement('canvas');\n    var context = canvas.getContext('2d');\n    var maxSizes = getAdjustedSizes({\n      aspectRatio: aspectRatio,\n      width: maxWidth,\n      height: maxHeight\n    });\n    var minSizes = getAdjustedSizes({\n      aspectRatio: aspectRatio,\n      width: minWidth,\n      height: minHeight\n    }, 'cover');\n    var width = Math.min(maxSizes.width, Math.max(minSizes.width, naturalWidth));\n    var height = Math.min(maxSizes.height, Math.max(minSizes.height, naturalHeight)); // Note: should always use image's natural sizes for drawing as\n    // imageData.naturalWidth === canvasData.naturalHeight when rotate % 180 === 90\n\n    var destMaxSizes = getAdjustedSizes({\n      aspectRatio: imageAspectRatio,\n      width: maxWidth,\n      height: maxHeight\n    });\n    var destMinSizes = getAdjustedSizes({\n      aspectRatio: imageAspectRatio,\n      width: minWidth,\n      height: minHeight\n    }, 'cover');\n    var destWidth = Math.min(destMaxSizes.width, Math.max(destMinSizes.width, imageNaturalWidth));\n    var destHeight = Math.min(destMaxSizes.height, Math.max(destMinSizes.height, imageNaturalHeight));\n    var params = [-destWidth / 2, -destHeight / 2, destWidth, destHeight];\n    canvas.width = normalizeDecimalNumber(width);\n    canvas.height = normalizeDecimalNumber(height);\n    context.fillStyle = fillColor;\n    context.fillRect(0, 0, width, height);\n    context.save();\n    context.translate(width / 2, height / 2);\n    context.rotate(rotate * Math.PI / 180);\n    context.scale(scaleX, scaleY);\n    context.imageSmoothingEnabled = imageSmoothingEnabled;\n    context.imageSmoothingQuality = imageSmoothingQuality;\n    /**\n     * ODOO FIX START\n     *\n     * Canevas is translated and then translated back. For the second translation the\n     * translation distances were rounded to the nearest integer below when it should\n     * not since the distances of the first translation are either an integer or the\n     * half of an integer.\n     *\n     * Fix proposed by https://github.com/fengyuanchen/cropperjs/pull/866\n     */\n    params = params.map(normalizeDecimalNumber);\n    context.drawImage(image, params[0], params[1], Math.floor(params[2]), Math.floor(params[3]));\n    // ODOO FIX END\n    context.restore();\n    return canvas;\n  }\n  var fromCharCode = String.fromCharCode;\n  /**\n   * Get string from char code in data view.\n   * @param {DataView} dataView - The data view for read.\n   * @param {number} start - The start index.\n   * @param {number} length - The read length.\n   * @returns {string} The read result.\n   */\n\n  function getStringFromCharCode(dataView, start, length) {\n    var str = '';\n    length += start;\n\n    for (var i = start; i < length; i += 1) {\n      str += fromCharCode(dataView.getUint8(i));\n    }\n\n    return str;\n  }\n  var REGEXP_DATA_URL_HEAD = /^data:.*,/;\n  /**\n   * Transform Data URL to array buffer.\n   * @param {string} dataURL - The Data URL to transform.\n   * @returns {ArrayBuffer} The result array buffer.\n   */\n\n  function dataURLToArrayBuffer(dataURL) {\n    var base64 = dataURL.replace(REGEXP_DATA_URL_HEAD, '');\n    var binary = atob(base64);\n    var arrayBuffer = new ArrayBuffer(binary.length);\n    var uint8 = new Uint8Array(arrayBuffer);\n    forEach(uint8, function (value, i) {\n      uint8[i] = binary.charCodeAt(i);\n    });\n    return arrayBuffer;\n  }\n  /**\n   * Transform array buffer to Data URL.\n   * @param {ArrayBuffer} arrayBuffer - The array buffer to transform.\n   * @param {string} mimeType - The mime type of the Data URL.\n   * @returns {string} The result Data URL.\n   */\n\n  function arrayBufferToDataURL(arrayBuffer, mimeType) {\n    var chunks = []; // Chunk Typed Array for better performance (#435)\n\n    var chunkSize = 8192;\n    var uint8 = new Uint8Array(arrayBuffer);\n\n    while (uint8.length > 0) {\n      // XXX: Babel's `toConsumableArray` helper will throw error in IE or Safari 9\n      // eslint-disable-next-line prefer-spread\n      chunks.push(fromCharCode.apply(null, toArray(uint8.subarray(0, chunkSize))));\n      uint8 = uint8.subarray(chunkSize);\n    }\n\n    return \"data:\".concat(mimeType, \";base64,\").concat(btoa(chunks.join('')));\n  }\n  /**\n   * Get orientation value from given array buffer.\n   * @param {ArrayBuffer} arrayBuffer - The array buffer to read.\n   * @returns {number} The read orientation value.\n   */\n\n  function resetAndGetOrientation(arrayBuffer) {\n    var dataView = new DataView(arrayBuffer);\n    var orientation; // Ignores range error when the image does not have correct Exif information\n\n    try {\n      var littleEndian;\n      var app1Start;\n      var ifdStart; // Only handle JPEG image (start by 0xFFD8)\n\n      if (dataView.getUint8(0) === 0xFF && dataView.getUint8(1) === 0xD8) {\n        var length = dataView.byteLength;\n        var offset = 2;\n\n        while (offset + 1 < length) {\n          if (dataView.getUint8(offset) === 0xFF && dataView.getUint8(offset + 1) === 0xE1) {\n            app1Start = offset;\n            break;\n          }\n\n          offset += 1;\n        }\n      }\n\n      if (app1Start) {\n        var exifIDCode = app1Start + 4;\n        var tiffOffset = app1Start + 10;\n\n        if (getStringFromCharCode(dataView, exifIDCode, 4) === 'Exif') {\n          var endianness = dataView.getUint16(tiffOffset);\n          littleEndian = endianness === 0x4949;\n\n          if (littleEndian || endianness === 0x4D4D\n          /* bigEndian */\n          ) {\n              if (dataView.getUint16(tiffOffset + 2, littleEndian) === 0x002A) {\n                var firstIFDOffset = dataView.getUint32(tiffOffset + 4, littleEndian);\n\n                if (firstIFDOffset >= 0x00000008) {\n                  ifdStart = tiffOffset + firstIFDOffset;\n                }\n              }\n            }\n        }\n      }\n\n      if (ifdStart) {\n        var _length = dataView.getUint16(ifdStart, littleEndian);\n\n        var _offset;\n\n        var i;\n\n        for (i = 0; i < _length; i += 1) {\n          _offset = ifdStart + i * 12 + 2;\n\n          if (dataView.getUint16(_offset, littleEndian) === 0x0112\n          /* Orientation */\n          ) {\n              // 8 is the offset of the current tag's value\n              _offset += 8; // Get the original orientation value\n\n              orientation = dataView.getUint16(_offset, littleEndian); // Override the orientation with its default value\n\n              dataView.setUint16(_offset, 1, littleEndian);\n              break;\n            }\n        }\n      }\n    } catch (error) {\n      orientation = 1;\n    }\n\n    return orientation;\n  }\n  /**\n   * Parse Exif Orientation value.\n   * @param {number} orientation - The orientation to parse.\n   * @returns {Object} The parsed result.\n   */\n\n  function parseOrientation(orientation) {\n    var rotate = 0;\n    var scaleX = 1;\n    var scaleY = 1;\n\n    switch (orientation) {\n      // Flip horizontal\n      case 2:\n        scaleX = -1;\n        break;\n      // Rotate left 180\u00b0\n\n      case 3:\n        rotate = -180;\n        break;\n      // Flip vertical\n\n      case 4:\n        scaleY = -1;\n        break;\n      // Flip vertical and rotate right 90\u00b0\n\n      case 5:\n        rotate = 90;\n        scaleY = -1;\n        break;\n      // Rotate right 90\u00b0\n\n      case 6:\n        rotate = 90;\n        break;\n      // Flip horizontal and rotate right 90\u00b0\n\n      case 7:\n        rotate = 90;\n        scaleX = -1;\n        break;\n      // Rotate left 90\u00b0\n\n      case 8:\n        rotate = -90;\n        break;\n\n      default:\n    }\n\n    return {\n      rotate: rotate,\n      scaleX: scaleX,\n      scaleY: scaleY\n    };\n  }\n\n  var render = {\n    render: function render() {\n      this.initContainer();\n      this.initCanvas();\n      this.initCropBox();\n      this.renderCanvas();\n\n      if (this.cropped) {\n        this.renderCropBox();\n      }\n    },\n    initContainer: function initContainer() {\n      var element = this.element,\n          options = this.options,\n          container = this.container,\n          cropper = this.cropper;\n      addClass(cropper, CLASS_HIDDEN);\n      removeClass(element, CLASS_HIDDEN);\n      var containerData = {\n        width: Math.max(container.offsetWidth, Number(options.minContainerWidth) || 200),\n        height: Math.max(container.offsetHeight, Number(options.minContainerHeight) || 100)\n      };\n      this.containerData = containerData;\n      setStyle(cropper, {\n        width: containerData.width,\n        height: containerData.height\n      });\n      addClass(element, CLASS_HIDDEN);\n      removeClass(cropper, CLASS_HIDDEN);\n    },\n    // Canvas (image wrapper)\n    initCanvas: function initCanvas() {\n      var containerData = this.containerData,\n          imageData = this.imageData;\n      var viewMode = this.options.viewMode;\n      var rotated = Math.abs(imageData.rotate) % 180 === 90;\n      var naturalWidth = rotated ? imageData.naturalHeight : imageData.naturalWidth;\n      var naturalHeight = rotated ? imageData.naturalWidth : imageData.naturalHeight;\n      var aspectRatio = naturalWidth / naturalHeight;\n      var canvasWidth = containerData.width;\n      var canvasHeight = containerData.height;\n\n      if (containerData.height * aspectRatio > containerData.width) {\n        if (viewMode === 3) {\n          canvasWidth = containerData.height * aspectRatio;\n        } else {\n          canvasHeight = containerData.width / aspectRatio;\n        }\n      } else if (viewMode === 3) {\n        canvasHeight = containerData.width / aspectRatio;\n      } else {\n        canvasWidth = containerData.height * aspectRatio;\n      }\n\n      var canvasData = {\n        aspectRatio: aspectRatio,\n        naturalWidth: naturalWidth,\n        naturalHeight: naturalHeight,\n        width: canvasWidth,\n        height: canvasHeight\n      };\n      canvasData.left = (containerData.width - canvasWidth) / 2;\n      canvasData.top = (containerData.height - canvasHeight) / 2;\n      canvasData.oldLeft = canvasData.left;\n      canvasData.oldTop = canvasData.top;\n      this.canvasData = canvasData;\n      this.limited = viewMode === 1 || viewMode === 2;\n      this.limitCanvas(true, true);\n      this.initialImageData = assign({}, imageData);\n      this.initialCanvasData = assign({}, canvasData);\n    },\n    limitCanvas: function limitCanvas(sizeLimited, positionLimited) {\n      var options = this.options,\n          containerData = this.containerData,\n          canvasData = this.canvasData,\n          cropBoxData = this.cropBoxData;\n      var viewMode = options.viewMode;\n      var aspectRatio = canvasData.aspectRatio;\n      var cropped = this.cropped && cropBoxData;\n\n      if (sizeLimited) {\n        var minCanvasWidth = Number(options.minCanvasWidth) || 0;\n        var minCanvasHeight = Number(options.minCanvasHeight) || 0;\n\n        if (viewMode > 1) {\n          minCanvasWidth = Math.max(minCanvasWidth, containerData.width);\n          minCanvasHeight = Math.max(minCanvasHeight, containerData.height);\n\n          if (viewMode === 3) {\n            if (minCanvasHeight * aspectRatio > minCanvasWidth) {\n              minCanvasWidth = minCanvasHeight * aspectRatio;\n            } else {\n              minCanvasHeight = minCanvasWidth / aspectRatio;\n            }\n          }\n        } else if (viewMode > 0) {\n          if (minCanvasWidth) {\n            minCanvasWidth = Math.max(minCanvasWidth, cropped ? cropBoxData.width : 0);\n          } else if (minCanvasHeight) {\n            minCanvasHeight = Math.max(minCanvasHeight, cropped ? cropBoxData.height : 0);\n          } else if (cropped) {\n            minCanvasWidth = cropBoxData.width;\n            minCanvasHeight = cropBoxData.height;\n\n            if (minCanvasHeight * aspectRatio > minCanvasWidth) {\n              minCanvasWidth = minCanvasHeight * aspectRatio;\n            } else {\n              minCanvasHeight = minCanvasWidth / aspectRatio;\n            }\n          }\n        }\n\n        var _getAdjustedSizes = getAdjustedSizes({\n          aspectRatio: aspectRatio,\n          width: minCanvasWidth,\n          height: minCanvasHeight\n        });\n\n        minCanvasWidth = _getAdjustedSizes.width;\n        minCanvasHeight = _getAdjustedSizes.height;\n        canvasData.minWidth = minCanvasWidth;\n        canvasData.minHeight = minCanvasHeight;\n        canvasData.maxWidth = Infinity;\n        canvasData.maxHeight = Infinity;\n      }\n\n      if (positionLimited) {\n        if (viewMode > (cropped ? 0 : 1)) {\n          var newCanvasLeft = containerData.width - canvasData.width;\n          var newCanvasTop = containerData.height - canvasData.height;\n          canvasData.minLeft = Math.min(0, newCanvasLeft);\n          canvasData.minTop = Math.min(0, newCanvasTop);\n          canvasData.maxLeft = Math.max(0, newCanvasLeft);\n          canvasData.maxTop = Math.max(0, newCanvasTop);\n\n          if (cropped && this.limited) {\n            canvasData.minLeft = Math.min(cropBoxData.left, cropBoxData.left + (cropBoxData.width - canvasData.width));\n            canvasData.minTop = Math.min(cropBoxData.top, cropBoxData.top + (cropBoxData.height - canvasData.height));\n            canvasData.maxLeft = cropBoxData.left;\n            canvasData.maxTop = cropBoxData.top;\n\n            if (viewMode === 2) {\n              if (canvasData.width >= containerData.width) {\n                canvasData.minLeft = Math.min(0, newCanvasLeft);\n                canvasData.maxLeft = Math.max(0, newCanvasLeft);\n              }\n\n              if (canvasData.height >= containerData.height) {\n                canvasData.minTop = Math.min(0, newCanvasTop);\n                canvasData.maxTop = Math.max(0, newCanvasTop);\n              }\n            }\n          }\n        } else {\n          canvasData.minLeft = -canvasData.width;\n          canvasData.minTop = -canvasData.height;\n          canvasData.maxLeft = containerData.width;\n          canvasData.maxTop = containerData.height;\n        }\n      }\n    },\n    renderCanvas: function renderCanvas(changed, transformed) {\n      var canvasData = this.canvasData,\n          imageData = this.imageData;\n\n      if (transformed) {\n        var _getRotatedSizes = getRotatedSizes({\n          width: imageData.naturalWidth * Math.abs(imageData.scaleX || 1),\n          height: imageData.naturalHeight * Math.abs(imageData.scaleY || 1),\n          degree: imageData.rotate || 0\n        }),\n            naturalWidth = _getRotatedSizes.width,\n            naturalHeight = _getRotatedSizes.height;\n\n        var width = canvasData.width * (naturalWidth / canvasData.naturalWidth);\n        var height = canvasData.height * (naturalHeight / canvasData.naturalHeight);\n        canvasData.left -= (width - canvasData.width) / 2;\n        canvasData.top -= (height - canvasData.height) / 2;\n        canvasData.width = width;\n        canvasData.height = height;\n        canvasData.aspectRatio = naturalWidth / naturalHeight;\n        canvasData.naturalWidth = naturalWidth;\n        canvasData.naturalHeight = naturalHeight;\n        this.limitCanvas(true, false);\n      }\n\n      if (canvasData.width > canvasData.maxWidth || canvasData.width < canvasData.minWidth) {\n        canvasData.left = canvasData.oldLeft;\n      }\n\n      if (canvasData.height > canvasData.maxHeight || canvasData.height < canvasData.minHeight) {\n        canvasData.top = canvasData.oldTop;\n      }\n\n      canvasData.width = Math.min(Math.max(canvasData.width, canvasData.minWidth), canvasData.maxWidth);\n      canvasData.height = Math.min(Math.max(canvasData.height, canvasData.minHeight), canvasData.maxHeight);\n      this.limitCanvas(false, true);\n      canvasData.left = Math.min(Math.max(canvasData.left, canvasData.minLeft), canvasData.maxLeft);\n      canvasData.top = Math.min(Math.max(canvasData.top, canvasData.minTop), canvasData.maxTop);\n      canvasData.oldLeft = canvasData.left;\n      canvasData.oldTop = canvasData.top;\n      setStyle(this.canvas, assign({\n        width: canvasData.width,\n        height: canvasData.height\n      }, getTransforms({\n        translateX: canvasData.left,\n        translateY: canvasData.top\n      })));\n      this.renderImage(changed);\n\n      if (this.cropped && this.limited) {\n        this.limitCropBox(true, true);\n      }\n    },\n    renderImage: function renderImage(changed) {\n      var canvasData = this.canvasData,\n          imageData = this.imageData;\n      var width = imageData.naturalWidth * (canvasData.width / canvasData.naturalWidth);\n      var height = imageData.naturalHeight * (canvasData.height / canvasData.naturalHeight);\n      assign(imageData, {\n        width: width,\n        height: height,\n        left: (canvasData.width - width) / 2,\n        top: (canvasData.height - height) / 2\n      });\n      setStyle(this.image, assign({\n        width: imageData.width,\n        height: imageData.height\n      }, getTransforms(assign({\n        translateX: imageData.left,\n        translateY: imageData.top\n      }, imageData))));\n\n      if (changed) {\n        this.output();\n      }\n    },\n    initCropBox: function initCropBox() {\n      var options = this.options,\n          canvasData = this.canvasData;\n      var aspectRatio = options.aspectRatio || options.initialAspectRatio;\n      var autoCropArea = Number(options.autoCropArea) || 0.8;\n      var cropBoxData = {\n        width: canvasData.width,\n        height: canvasData.height\n      };\n\n      if (aspectRatio) {\n        if (canvasData.height * aspectRatio > canvasData.width) {\n          cropBoxData.height = cropBoxData.width / aspectRatio;\n        } else {\n          cropBoxData.width = cropBoxData.height * aspectRatio;\n        }\n      }\n\n      this.cropBoxData = cropBoxData;\n      this.limitCropBox(true, true); // Initialize auto crop area\n\n      cropBoxData.width = Math.min(Math.max(cropBoxData.width, cropBoxData.minWidth), cropBoxData.maxWidth);\n      cropBoxData.height = Math.min(Math.max(cropBoxData.height, cropBoxData.minHeight), cropBoxData.maxHeight); // The width/height of auto crop area must large than \"minWidth/Height\"\n\n      cropBoxData.width = Math.max(cropBoxData.minWidth, cropBoxData.width * autoCropArea);\n      cropBoxData.height = Math.max(cropBoxData.minHeight, cropBoxData.height * autoCropArea);\n      cropBoxData.left = canvasData.left + (canvasData.width - cropBoxData.width) / 2;\n      cropBoxData.top = canvasData.top + (canvasData.height - cropBoxData.height) / 2;\n      cropBoxData.oldLeft = cropBoxData.left;\n      cropBoxData.oldTop = cropBoxData.top;\n      this.initialCropBoxData = assign({}, cropBoxData);\n    },\n    limitCropBox: function limitCropBox(sizeLimited, positionLimited) {\n      var options = this.options,\n          containerData = this.containerData,\n          canvasData = this.canvasData,\n          cropBoxData = this.cropBoxData,\n          limited = this.limited;\n      var aspectRatio = options.aspectRatio;\n\n      if (sizeLimited) {\n        var minCropBoxWidth = Number(options.minCropBoxWidth) || 0;\n        var minCropBoxHeight = Number(options.minCropBoxHeight) || 0;\n        var maxCropBoxWidth = limited ? Math.min(containerData.width, canvasData.width, canvasData.width + canvasData.left, containerData.width - canvasData.left) : containerData.width;\n        var maxCropBoxHeight = limited ? Math.min(containerData.height, canvasData.height, canvasData.height + canvasData.top, containerData.height - canvasData.top) : containerData.height; // The min/maxCropBoxWidth/Height must be less than container's width/height\n\n        minCropBoxWidth = Math.min(minCropBoxWidth, containerData.width);\n        minCropBoxHeight = Math.min(minCropBoxHeight, containerData.height);\n\n        if (aspectRatio) {\n          if (minCropBoxWidth && minCropBoxHeight) {\n            if (minCropBoxHeight * aspectRatio > minCropBoxWidth) {\n              minCropBoxHeight = minCropBoxWidth / aspectRatio;\n            } else {\n              minCropBoxWidth = minCropBoxHeight * aspectRatio;\n            }\n          } else if (minCropBoxWidth) {\n            minCropBoxHeight = minCropBoxWidth / aspectRatio;\n          } else if (minCropBoxHeight) {\n            minCropBoxWidth = minCropBoxHeight * aspectRatio;\n          }\n\n          if (maxCropBoxHeight * aspectRatio > maxCropBoxWidth) {\n            maxCropBoxHeight = maxCropBoxWidth / aspectRatio;\n          } else {\n            maxCropBoxWidth = maxCropBoxHeight * aspectRatio;\n          }\n        } // The minWidth/Height must be less than maxWidth/Height\n\n\n        cropBoxData.minWidth = Math.min(minCropBoxWidth, maxCropBoxWidth);\n        cropBoxData.minHeight = Math.min(minCropBoxHeight, maxCropBoxHeight);\n        cropBoxData.maxWidth = maxCropBoxWidth;\n        cropBoxData.maxHeight = maxCropBoxHeight;\n      }\n\n      if (positionLimited) {\n        if (limited) {\n          cropBoxData.minLeft = Math.max(0, canvasData.left);\n          cropBoxData.minTop = Math.max(0, canvasData.top);\n          cropBoxData.maxLeft = Math.min(containerData.width, canvasData.left + canvasData.width) - cropBoxData.width;\n          cropBoxData.maxTop = Math.min(containerData.height, canvasData.top + canvasData.height) - cropBoxData.height;\n        } else {\n          cropBoxData.minLeft = 0;\n          cropBoxData.minTop = 0;\n          cropBoxData.maxLeft = containerData.width - cropBoxData.width;\n          cropBoxData.maxTop = containerData.height - cropBoxData.height;\n        }\n      }\n    },\n    renderCropBox: function renderCropBox() {\n      var options = this.options,\n          containerData = this.containerData,\n          cropBoxData = this.cropBoxData;\n\n      if (cropBoxData.width > cropBoxData.maxWidth || cropBoxData.width < cropBoxData.minWidth) {\n        cropBoxData.left = cropBoxData.oldLeft;\n      }\n\n      if (cropBoxData.height > cropBoxData.maxHeight || cropBoxData.height < cropBoxData.minHeight) {\n        cropBoxData.top = cropBoxData.oldTop;\n      }\n\n      cropBoxData.width = Math.min(Math.max(cropBoxData.width, cropBoxData.minWidth), cropBoxData.maxWidth);\n      cropBoxData.height = Math.min(Math.max(cropBoxData.height, cropBoxData.minHeight), cropBoxData.maxHeight);\n      this.limitCropBox(false, true);\n      cropBoxData.left = Math.min(Math.max(cropBoxData.left, cropBoxData.minLeft), cropBoxData.maxLeft);\n      cropBoxData.top = Math.min(Math.max(cropBoxData.top, cropBoxData.minTop), cropBoxData.maxTop);\n      cropBoxData.oldLeft = cropBoxData.left;\n      cropBoxData.oldTop = cropBoxData.top;\n\n      if (options.movable && options.cropBoxMovable) {\n        // Turn to move the canvas when the crop box is equal to the container\n        setData(this.face, DATA_ACTION, cropBoxData.width >= containerData.width && cropBoxData.height >= containerData.height ? ACTION_MOVE : ACTION_ALL);\n      }\n\n      setStyle(this.cropBox, assign({\n        width: cropBoxData.width,\n        height: cropBoxData.height\n      }, getTransforms({\n        translateX: cropBoxData.left,\n        translateY: cropBoxData.top\n      })));\n\n      if (this.cropped && this.limited) {\n        this.limitCanvas(true, true);\n      }\n\n      if (!this.disabled) {\n        this.output();\n      }\n    },\n    output: function output() {\n      this.preview();\n      dispatchEvent(this.element, EVENT_CROP, this.getData());\n    }\n  };\n\n  var preview = {\n    initPreview: function initPreview() {\n      var element = this.element,\n          crossOrigin = this.crossOrigin;\n      var preview = this.options.preview;\n      var url = crossOrigin ? this.crossOriginUrl : this.url;\n      var alt = element.alt || 'The image to preview';\n      var image = document.createElement('img');\n\n      if (crossOrigin) {\n        image.crossOrigin = crossOrigin;\n      }\n\n      image.src = url;\n      image.alt = alt;\n      this.viewBox.appendChild(image);\n      this.viewBoxImage = image;\n\n      if (!preview) {\n        return;\n      }\n\n      var previews = preview;\n\n      if (typeof preview === 'string') {\n        previews = element.ownerDocument.querySelectorAll(preview);\n      } else if (preview.querySelector) {\n        previews = [preview];\n      }\n\n      this.previews = previews;\n      forEach(previews, function (el) {\n        var img = document.createElement('img'); // Save the original size for recover\n\n        setData(el, DATA_PREVIEW, {\n          width: el.offsetWidth,\n          height: el.offsetHeight,\n          html: el.innerHTML\n        });\n\n        if (crossOrigin) {\n          img.crossOrigin = crossOrigin;\n        }\n\n        img.src = url;\n        img.alt = alt;\n        /**\n         * Override img element styles\n         * Add `display:block` to avoid margin top issue\n         * Add `height:auto` to override `height` attribute on IE8\n         * (Occur only when margin-top <= -height)\n         */\n\n        img.style.cssText = 'display:block;' + 'width:100%;' + 'height:auto;' + 'min-width:0!important;' + 'min-height:0!important;' + 'max-width:none!important;' + 'max-height:none!important;' + 'image-orientation:0deg!important;\"';\n        el.innerHTML = '';\n        el.appendChild(img);\n      });\n    },\n    resetPreview: function resetPreview() {\n      forEach(this.previews, function (element) {\n        var data = getData(element, DATA_PREVIEW);\n        setStyle(element, {\n          width: data.width,\n          height: data.height\n        });\n        element.innerHTML = data.html;\n        removeData(element, DATA_PREVIEW);\n      });\n    },\n    preview: function preview() {\n      var imageData = this.imageData,\n          canvasData = this.canvasData,\n          cropBoxData = this.cropBoxData;\n      var cropBoxWidth = cropBoxData.width,\n          cropBoxHeight = cropBoxData.height;\n      var width = imageData.width,\n          height = imageData.height;\n      var left = cropBoxData.left - canvasData.left - imageData.left;\n      var top = cropBoxData.top - canvasData.top - imageData.top;\n\n      if (!this.cropped || this.disabled) {\n        return;\n      }\n\n      setStyle(this.viewBoxImage, assign({\n        width: width,\n        height: height\n      }, getTransforms(assign({\n        translateX: -left,\n        translateY: -top\n      }, imageData))));\n      forEach(this.previews, function (element) {\n        var data = getData(element, DATA_PREVIEW);\n        var originalWidth = data.width;\n        var originalHeight = data.height;\n        var newWidth = originalWidth;\n        var newHeight = originalHeight;\n        var ratio = 1;\n\n        if (cropBoxWidth) {\n          ratio = originalWidth / cropBoxWidth;\n          newHeight = cropBoxHeight * ratio;\n        }\n\n        if (cropBoxHeight && newHeight > originalHeight) {\n          ratio = originalHeight / cropBoxHeight;\n          newWidth = cropBoxWidth * ratio;\n          newHeight = originalHeight;\n        }\n\n        setStyle(element, {\n          width: newWidth,\n          height: newHeight\n        });\n        setStyle(element.getElementsByTagName('img')[0], assign({\n          width: width * ratio,\n          height: height * ratio\n        }, getTransforms(assign({\n          translateX: -left * ratio,\n          translateY: -top * ratio\n        }, imageData))));\n      });\n    }\n  };\n\n  var events = {\n    bind: function bind() {\n      var element = this.element,\n          options = this.options,\n          cropper = this.cropper;\n\n      if (isFunction(options.cropstart)) {\n        addListener(element, EVENT_CROP_START, options.cropstart);\n      }\n\n      if (isFunction(options.cropmove)) {\n        addListener(element, EVENT_CROP_MOVE, options.cropmove);\n      }\n\n      if (isFunction(options.cropend)) {\n        addListener(element, EVENT_CROP_END, options.cropend);\n      }\n\n      if (isFunction(options.crop)) {\n        addListener(element, EVENT_CROP, options.crop);\n      }\n\n      if (isFunction(options.zoom)) {\n        addListener(element, EVENT_ZOOM, options.zoom);\n      }\n\n      addListener(cropper, EVENT_POINTER_DOWN, this.onCropStart = this.cropStart.bind(this));\n\n      if (options.zoomable && options.zoomOnWheel) {\n        addListener(cropper, EVENT_WHEEL, this.onWheel = this.wheel.bind(this), {\n          passive: false,\n          capture: true\n        });\n      }\n\n      if (options.toggleDragModeOnDblclick) {\n        addListener(cropper, EVENT_DBLCLICK, this.onDblclick = this.dblclick.bind(this));\n      }\n\n      addListener(element.ownerDocument, EVENT_POINTER_MOVE, this.onCropMove = this.cropMove.bind(this));\n      addListener(element.ownerDocument, EVENT_POINTER_UP, this.onCropEnd = this.cropEnd.bind(this));\n\n      if (options.responsive) {\n        addListener(window, EVENT_RESIZE, this.onResize = this.resize.bind(this));\n      }\n    },\n    unbind: function unbind() {\n      var element = this.element,\n          options = this.options,\n          cropper = this.cropper;\n\n      if (isFunction(options.cropstart)) {\n        removeListener(element, EVENT_CROP_START, options.cropstart);\n      }\n\n      if (isFunction(options.cropmove)) {\n        removeListener(element, EVENT_CROP_MOVE, options.cropmove);\n      }\n\n      if (isFunction(options.cropend)) {\n        removeListener(element, EVENT_CROP_END, options.cropend);\n      }\n\n      if (isFunction(options.crop)) {\n        removeListener(element, EVENT_CROP, options.crop);\n      }\n\n      if (isFunction(options.zoom)) {\n        removeListener(element, EVENT_ZOOM, options.zoom);\n      }\n\n      removeListener(cropper, EVENT_POINTER_DOWN, this.onCropStart);\n\n      if (options.zoomable && options.zoomOnWheel) {\n        removeListener(cropper, EVENT_WHEEL, this.onWheel, {\n          passive: false,\n          capture: true\n        });\n      }\n\n      if (options.toggleDragModeOnDblclick) {\n        removeListener(cropper, EVENT_DBLCLICK, this.onDblclick);\n      }\n\n      removeListener(element.ownerDocument, EVENT_POINTER_MOVE, this.onCropMove);\n      removeListener(element.ownerDocument, EVENT_POINTER_UP, this.onCropEnd);\n\n      if (options.responsive) {\n        removeListener(window, EVENT_RESIZE, this.onResize);\n      }\n    }\n  };\n\n  var handlers = {\n    resize: function resize() {\n      var options = this.options,\n          container = this.container,\n          containerData = this.containerData;\n      var minContainerWidth = Number(options.minContainerWidth) || MIN_CONTAINER_WIDTH;\n      var minContainerHeight = Number(options.minContainerHeight) || MIN_CONTAINER_HEIGHT;\n\n      if (this.disabled || containerData.width <= minContainerWidth || containerData.height <= minContainerHeight) {\n        return;\n      }\n\n      var ratio = container.offsetWidth / containerData.width; // Resize when width changed or height changed\n\n      if (ratio !== 1 || container.offsetHeight !== containerData.height) {\n        var canvasData;\n        var cropBoxData;\n\n        if (options.restore) {\n          canvasData = this.getCanvasData();\n          cropBoxData = this.getCropBoxData();\n        }\n\n        this.render();\n\n        if (options.restore) {\n          this.setCanvasData(forEach(canvasData, function (n, i) {\n            canvasData[i] = n * ratio;\n          }));\n          this.setCropBoxData(forEach(cropBoxData, function (n, i) {\n            cropBoxData[i] = n * ratio;\n          }));\n        }\n      }\n    },\n    dblclick: function dblclick() {\n      if (this.disabled || this.options.dragMode === DRAG_MODE_NONE) {\n        return;\n      }\n\n      this.setDragMode(hasClass(this.dragBox, CLASS_CROP) ? DRAG_MODE_MOVE : DRAG_MODE_CROP);\n    },\n    wheel: function wheel(event) {\n      var _this = this;\n\n      var ratio = Number(this.options.wheelZoomRatio) || 0.1;\n      var delta = 1;\n\n      if (this.disabled) {\n        return;\n      }\n\n      event.preventDefault(); // Limit wheel speed to prevent zoom too fast (#21)\n\n      if (this.wheeling) {\n        return;\n      }\n\n      this.wheeling = true;\n      setTimeout(function () {\n        _this.wheeling = false;\n      }, 50);\n\n      if (event.deltaY) {\n        delta = event.deltaY > 0 ? 1 : -1;\n      } else if (event.wheelDelta) {\n        delta = -event.wheelDelta / 120;\n      } else if (event.detail) {\n        delta = event.detail > 0 ? 1 : -1;\n      }\n\n      this.zoom(-delta * ratio, event);\n    },\n    cropStart: function cropStart(event) {\n      var buttons = event.buttons,\n          button = event.button;\n\n      if (this.disabled // No primary button (Usually the left button)\n      // Note that touch events have no `buttons` or `button` property\n      || isNumber(buttons) && buttons !== 1 || isNumber(button) && button !== 0 // Open context menu\n      || event.ctrlKey) {\n        return;\n      }\n\n      var options = this.options,\n          pointers = this.pointers;\n      var action;\n\n      if (event.changedTouches) {\n        // Handle touch event\n        forEach(event.changedTouches, function (touch) {\n          pointers[touch.identifier] = getPointer(touch);\n        });\n      } else {\n        // Handle mouse event and pointer event\n        pointers[event.pointerId || 0] = getPointer(event);\n      }\n\n      if (Object.keys(pointers).length > 1 && options.zoomable && options.zoomOnTouch) {\n        action = ACTION_ZOOM;\n      } else {\n        action = getData(event.target, DATA_ACTION);\n      }\n\n      if (!REGEXP_ACTIONS.test(action)) {\n        return;\n      }\n\n      if (dispatchEvent(this.element, EVENT_CROP_START, {\n        originalEvent: event,\n        action: action\n      }) === false) {\n        return;\n      } // This line is required for preventing page zooming in iOS browsers\n\n\n      event.preventDefault();\n      this.action = action;\n      this.cropping = false;\n\n      if (action === ACTION_CROP) {\n        this.cropping = true;\n        addClass(this.dragBox, CLASS_MODAL);\n      }\n    },\n    cropMove: function cropMove(event) {\n      var action = this.action;\n\n      if (this.disabled || !action) {\n        return;\n      }\n\n      var pointers = this.pointers;\n      event.preventDefault();\n\n      if (dispatchEvent(this.element, EVENT_CROP_MOVE, {\n        originalEvent: event,\n        action: action\n      }) === false) {\n        return;\n      }\n\n      if (event.changedTouches) {\n        forEach(event.changedTouches, function (touch) {\n          // The first parameter should not be undefined (#432)\n          assign(pointers[touch.identifier] || {}, getPointer(touch, true));\n        });\n      } else {\n        assign(pointers[event.pointerId || 0] || {}, getPointer(event, true));\n      }\n\n      this.change(event);\n    },\n    cropEnd: function cropEnd(event) {\n      if (this.disabled) {\n        return;\n      }\n\n      var action = this.action,\n          pointers = this.pointers;\n\n      if (event.changedTouches) {\n        forEach(event.changedTouches, function (touch) {\n          delete pointers[touch.identifier];\n        });\n      } else {\n        delete pointers[event.pointerId || 0];\n      }\n\n      if (!action) {\n        return;\n      }\n\n      event.preventDefault();\n\n      if (!Object.keys(pointers).length) {\n        this.action = '';\n      }\n\n      if (this.cropping) {\n        this.cropping = false;\n        toggleClass(this.dragBox, CLASS_MODAL, this.cropped && this.options.modal);\n      }\n\n      dispatchEvent(this.element, EVENT_CROP_END, {\n        originalEvent: event,\n        action: action\n      });\n    }\n  };\n\n  var change = {\n    change: function change(event) {\n      var options = this.options,\n          canvasData = this.canvasData,\n          containerData = this.containerData,\n          cropBoxData = this.cropBoxData,\n          pointers = this.pointers;\n      var action = this.action;\n      var aspectRatio = options.aspectRatio;\n      var left = cropBoxData.left,\n          top = cropBoxData.top,\n          width = cropBoxData.width,\n          height = cropBoxData.height;\n      var right = left + width;\n      var bottom = top + height;\n      var minLeft = 0;\n      var minTop = 0;\n      var maxWidth = containerData.width;\n      var maxHeight = containerData.height;\n      var renderable = true;\n      var offset; // Locking aspect ratio in \"free mode\" by holding shift key\n\n      if (!aspectRatio && event.shiftKey) {\n        aspectRatio = width && height ? width / height : 1;\n      }\n\n      if (this.limited) {\n        minLeft = cropBoxData.minLeft;\n        minTop = cropBoxData.minTop;\n        maxWidth = minLeft + Math.min(containerData.width, canvasData.width, canvasData.left + canvasData.width);\n        maxHeight = minTop + Math.min(containerData.height, canvasData.height, canvasData.top + canvasData.height);\n      }\n\n      var pointer = pointers[Object.keys(pointers)[0]];\n      var range = {\n        x: pointer.endX - pointer.startX,\n        y: pointer.endY - pointer.startY\n      };\n\n      var check = function check(side) {\n        switch (side) {\n          case ACTION_EAST:\n            if (right + range.x > maxWidth) {\n              range.x = maxWidth - right;\n            }\n\n            break;\n\n          case ACTION_WEST:\n            if (left + range.x < minLeft) {\n              range.x = minLeft - left;\n            }\n\n            break;\n\n          case ACTION_NORTH:\n            if (top + range.y < minTop) {\n              range.y = minTop - top;\n            }\n\n            break;\n\n          case ACTION_SOUTH:\n            if (bottom + range.y > maxHeight) {\n              range.y = maxHeight - bottom;\n            }\n\n            break;\n\n          default:\n        }\n      };\n\n      switch (action) {\n        // Move crop box\n        case ACTION_ALL:\n          left += range.x;\n          top += range.y;\n          break;\n        // Resize crop box\n\n        case ACTION_EAST:\n          if (range.x >= 0 && (right >= maxWidth || aspectRatio && (top <= minTop || bottom >= maxHeight))) {\n            renderable = false;\n            break;\n          }\n\n          check(ACTION_EAST);\n          width += range.x;\n\n          if (width < 0) {\n            action = ACTION_WEST;\n            width = -width;\n            left -= width;\n          }\n\n          if (aspectRatio) {\n            height = width / aspectRatio;\n            top += (cropBoxData.height - height) / 2;\n          }\n\n          break;\n\n        case ACTION_NORTH:\n          if (range.y <= 0 && (top <= minTop || aspectRatio && (left <= minLeft || right >= maxWidth))) {\n            renderable = false;\n            break;\n          }\n\n          check(ACTION_NORTH);\n          height -= range.y;\n          top += range.y;\n\n          if (height < 0) {\n            action = ACTION_SOUTH;\n            height = -height;\n            top -= height;\n          }\n\n          if (aspectRatio) {\n            width = height * aspectRatio;\n            left += (cropBoxData.width - width) / 2;\n          }\n\n          break;\n\n        case ACTION_WEST:\n          if (range.x <= 0 && (left <= minLeft || aspectRatio && (top <= minTop || bottom >= maxHeight))) {\n            renderable = false;\n            break;\n          }\n\n          check(ACTION_WEST);\n          width -= range.x;\n          left += range.x;\n\n          if (width < 0) {\n            action = ACTION_EAST;\n            width = -width;\n            left -= width;\n          }\n\n          if (aspectRatio) {\n            height = width / aspectRatio;\n            top += (cropBoxData.height - height) / 2;\n          }\n\n          break;\n\n        case ACTION_SOUTH:\n          if (range.y >= 0 && (bottom >= maxHeight || aspectRatio && (left <= minLeft || right >= maxWidth))) {\n            renderable = false;\n            break;\n          }\n\n          check(ACTION_SOUTH);\n          height += range.y;\n\n          if (height < 0) {\n            action = ACTION_NORTH;\n            height = -height;\n            top -= height;\n          }\n\n          if (aspectRatio) {\n            width = height * aspectRatio;\n            left += (cropBoxData.width - width) / 2;\n          }\n\n          break;\n\n        case ACTION_NORTH_EAST:\n          if (aspectRatio) {\n            if (range.y <= 0 && (top <= minTop || right >= maxWidth)) {\n              renderable = false;\n              break;\n            }\n\n            check(ACTION_NORTH);\n            height -= range.y;\n            top += range.y;\n            width = height * aspectRatio;\n          } else {\n            check(ACTION_NORTH);\n            check(ACTION_EAST);\n\n            if (range.x >= 0) {\n              if (right < maxWidth) {\n                width += range.x;\n              } else if (range.y <= 0 && top <= minTop) {\n                renderable = false;\n              }\n            } else {\n              width += range.x;\n            }\n\n            if (range.y <= 0) {\n              if (top > minTop) {\n                height -= range.y;\n                top += range.y;\n              }\n            } else {\n              height -= range.y;\n              top += range.y;\n            }\n          }\n\n          if (width < 0 && height < 0) {\n            action = ACTION_SOUTH_WEST;\n            height = -height;\n            width = -width;\n            top -= height;\n            left -= width;\n          } else if (width < 0) {\n            action = ACTION_NORTH_WEST;\n            width = -width;\n            left -= width;\n          } else if (height < 0) {\n            action = ACTION_SOUTH_EAST;\n            height = -height;\n            top -= height;\n          }\n\n          break;\n\n        case ACTION_NORTH_WEST:\n          if (aspectRatio) {\n            if (range.y <= 0 && (top <= minTop || left <= minLeft)) {\n              renderable = false;\n              break;\n            }\n\n            check(ACTION_NORTH);\n            height -= range.y;\n            top += range.y;\n            width = height * aspectRatio;\n            left += cropBoxData.width - width;\n          } else {\n            check(ACTION_NORTH);\n            check(ACTION_WEST);\n\n            if (range.x <= 0) {\n              if (left > minLeft) {\n                width -= range.x;\n                left += range.x;\n              } else if (range.y <= 0 && top <= minTop) {\n                renderable = false;\n              }\n            } else {\n              width -= range.x;\n              left += range.x;\n            }\n\n            if (range.y <= 0) {\n              if (top > minTop) {\n                height -= range.y;\n                top += range.y;\n              }\n            } else {\n              height -= range.y;\n              top += range.y;\n            }\n          }\n\n          if (width < 0 && height < 0) {\n            action = ACTION_SOUTH_EAST;\n            height = -height;\n            width = -width;\n            top -= height;\n            left -= width;\n          } else if (width < 0) {\n            action = ACTION_NORTH_EAST;\n            width = -width;\n            left -= width;\n          } else if (height < 0) {\n            action = ACTION_SOUTH_WEST;\n            height = -height;\n            top -= height;\n          }\n\n          break;\n\n        case ACTION_SOUTH_WEST:\n          if (aspectRatio) {\n            if (range.x <= 0 && (left <= minLeft || bottom >= maxHeight)) {\n              renderable = false;\n              break;\n            }\n\n            check(ACTION_WEST);\n            width -= range.x;\n            left += range.x;\n            height = width / aspectRatio;\n          } else {\n            check(ACTION_SOUTH);\n            check(ACTION_WEST);\n\n            if (range.x <= 0) {\n              if (left > minLeft) {\n                width -= range.x;\n                left += range.x;\n              } else if (range.y >= 0 && bottom >= maxHeight) {\n                renderable = false;\n              }\n            } else {\n              width -= range.x;\n              left += range.x;\n            }\n\n            if (range.y >= 0) {\n              if (bottom < maxHeight) {\n                height += range.y;\n              }\n            } else {\n              height += range.y;\n            }\n          }\n\n          if (width < 0 && height < 0) {\n            action = ACTION_NORTH_EAST;\n            height = -height;\n            width = -width;\n            top -= height;\n            left -= width;\n          } else if (width < 0) {\n            action = ACTION_SOUTH_EAST;\n            width = -width;\n            left -= width;\n          } else if (height < 0) {\n            action = ACTION_NORTH_WEST;\n            height = -height;\n            top -= height;\n          }\n\n          break;\n\n        case ACTION_SOUTH_EAST:\n          if (aspectRatio) {\n            if (range.x >= 0 && (right >= maxWidth || bottom >= maxHeight)) {\n              renderable = false;\n              break;\n            }\n\n            check(ACTION_EAST);\n            width += range.x;\n            height = width / aspectRatio;\n          } else {\n            check(ACTION_SOUTH);\n            check(ACTION_EAST);\n\n            if (range.x >= 0) {\n              if (right < maxWidth) {\n                width += range.x;\n              } else if (range.y >= 0 && bottom >= maxHeight) {\n                renderable = false;\n              }\n            } else {\n              width += range.x;\n            }\n\n            if (range.y >= 0) {\n              if (bottom < maxHeight) {\n                height += range.y;\n              }\n            } else {\n              height += range.y;\n            }\n          }\n\n          if (width < 0 && height < 0) {\n            action = ACTION_NORTH_WEST;\n            height = -height;\n            width = -width;\n            top -= height;\n            left -= width;\n          } else if (width < 0) {\n            action = ACTION_SOUTH_WEST;\n            width = -width;\n            left -= width;\n          } else if (height < 0) {\n            action = ACTION_NORTH_EAST;\n            height = -height;\n            top -= height;\n          }\n\n          break;\n        // Move canvas\n\n        case ACTION_MOVE:\n          this.move(range.x, range.y);\n          renderable = false;\n          break;\n        // Zoom canvas\n\n        case ACTION_ZOOM:\n          this.zoom(getMaxZoomRatio(pointers), event);\n          renderable = false;\n          break;\n        // Create crop box\n\n        case ACTION_CROP:\n          if (!range.x || !range.y) {\n            renderable = false;\n            break;\n          }\n\n          offset = getOffset(this.cropper);\n          left = pointer.startX - offset.left;\n          top = pointer.startY - offset.top;\n          width = cropBoxData.minWidth;\n          height = cropBoxData.minHeight;\n\n          if (range.x > 0) {\n            action = range.y > 0 ? ACTION_SOUTH_EAST : ACTION_NORTH_EAST;\n          } else if (range.x < 0) {\n            left -= width;\n            action = range.y > 0 ? ACTION_SOUTH_WEST : ACTION_NORTH_WEST;\n          }\n\n          if (range.y < 0) {\n            top -= height;\n          } // Show the crop box if is hidden\n\n\n          if (!this.cropped) {\n            removeClass(this.cropBox, CLASS_HIDDEN);\n            this.cropped = true;\n\n            if (this.limited) {\n              this.limitCropBox(true, true);\n            }\n          }\n\n          break;\n\n        default:\n      }\n\n      if (renderable) {\n        cropBoxData.width = width;\n        cropBoxData.height = height;\n        cropBoxData.left = left;\n        cropBoxData.top = top;\n        this.action = action;\n        this.renderCropBox();\n      } // Override\n\n\n      forEach(pointers, function (p) {\n        p.startX = p.endX;\n        p.startY = p.endY;\n      });\n    }\n  };\n\n  var methods = {\n    // Show the crop box manually\n    crop: function crop() {\n      if (this.ready && !this.cropped && !this.disabled) {\n        this.cropped = true;\n        this.limitCropBox(true, true);\n\n        if (this.options.modal) {\n          addClass(this.dragBox, CLASS_MODAL);\n        }\n\n        removeClass(this.cropBox, CLASS_HIDDEN);\n        this.setCropBoxData(this.initialCropBoxData);\n      }\n\n      return this;\n    },\n    // Reset the image and crop box to their initial states\n    reset: function reset() {\n      if (this.ready && !this.disabled) {\n        this.imageData = assign({}, this.initialImageData);\n        this.canvasData = assign({}, this.initialCanvasData);\n        this.cropBoxData = assign({}, this.initialCropBoxData);\n        this.renderCanvas();\n\n        if (this.cropped) {\n          this.renderCropBox();\n        }\n      }\n\n      return this;\n    },\n    // Clear the crop box\n    clear: function clear() {\n      if (this.cropped && !this.disabled) {\n        assign(this.cropBoxData, {\n          left: 0,\n          top: 0,\n          width: 0,\n          height: 0\n        });\n        this.cropped = false;\n        this.renderCropBox();\n        this.limitCanvas(true, true); // Render canvas after crop box rendered\n\n        this.renderCanvas();\n        removeClass(this.dragBox, CLASS_MODAL);\n        addClass(this.cropBox, CLASS_HIDDEN);\n      }\n\n      return this;\n    },\n\n    /**\n     * Replace the image's src and rebuild the cropper\n     * @param {string} url - The new URL.\n     * @param {boolean} [hasSameSize] - Indicate if the new image has the same size as the old one.\n     * @returns {Cropper} this\n     */\n    replace: function replace(url) {\n      var hasSameSize = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;\n\n      if (!this.disabled && url) {\n        if (this.isImg) {\n          this.element.src = url;\n        }\n\n        if (hasSameSize) {\n          this.url = url;\n          this.image.src = url;\n\n          if (this.ready) {\n            this.viewBoxImage.src = url;\n            forEach(this.previews, function (element) {\n              element.getElementsByTagName('img')[0].src = url;\n            });\n          }\n        } else {\n          if (this.isImg) {\n            this.replaced = true;\n          }\n\n          this.options.data = null;\n          this.uncreate();\n          this.load(url);\n        }\n      }\n\n      return this;\n    },\n    // Enable (unfreeze) the cropper\n    enable: function enable() {\n      if (this.ready && this.disabled) {\n        this.disabled = false;\n        removeClass(this.cropper, CLASS_DISABLED);\n      }\n\n      return this;\n    },\n    // Disable (freeze) the cropper\n    disable: function disable() {\n      if (this.ready && !this.disabled) {\n        this.disabled = true;\n        addClass(this.cropper, CLASS_DISABLED);\n      }\n\n      return this;\n    },\n\n    /**\n     * Destroy the cropper and remove the instance from the image\n     * @returns {Cropper} this\n     */\n    destroy: function destroy() {\n      var element = this.element;\n\n      if (!element[NAMESPACE]) {\n        return this;\n      }\n\n      element[NAMESPACE] = undefined;\n\n      if (this.isImg && this.replaced) {\n        element.src = this.originalUrl;\n      }\n\n      this.uncreate();\n      return this;\n    },\n\n    /**\n     * Move the canvas with relative offsets\n     * @param {number} offsetX - The relative offset distance on the x-axis.\n     * @param {number} [offsetY=offsetX] - The relative offset distance on the y-axis.\n     * @returns {Cropper} this\n     */\n    move: function move(offsetX) {\n      var offsetY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : offsetX;\n      var _this$canvasData = this.canvasData,\n          left = _this$canvasData.left,\n          top = _this$canvasData.top;\n      return this.moveTo(isUndefined(offsetX) ? offsetX : left + Number(offsetX), isUndefined(offsetY) ? offsetY : top + Number(offsetY));\n    },\n\n    /**\n     * Move the canvas to an absolute point\n     * @param {number} x - The x-axis coordinate.\n     * @param {number} [y=x] - The y-axis coordinate.\n     * @returns {Cropper} this\n     */\n    moveTo: function moveTo(x) {\n      var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : x;\n      var canvasData = this.canvasData;\n      var changed = false;\n      x = Number(x);\n      y = Number(y);\n\n      if (this.ready && !this.disabled && this.options.movable) {\n        if (isNumber(x)) {\n          canvasData.left = x;\n          changed = true;\n        }\n\n        if (isNumber(y)) {\n          canvasData.top = y;\n          changed = true;\n        }\n\n        if (changed) {\n          this.renderCanvas(true);\n        }\n      }\n\n      return this;\n    },\n\n    /**\n     * Zoom the canvas with a relative ratio\n     * @param {number} ratio - The target ratio.\n     * @param {Event} _originalEvent - The original event if any.\n     * @returns {Cropper} this\n     */\n    zoom: function zoom(ratio, _originalEvent) {\n      var canvasData = this.canvasData;\n      ratio = Number(ratio);\n\n      if (ratio < 0) {\n        ratio = 1 / (1 - ratio);\n      } else {\n        ratio = 1 + ratio;\n      }\n\n      return this.zoomTo(canvasData.width * ratio / canvasData.naturalWidth, null, _originalEvent);\n    },\n\n    /**\n     * Zoom the canvas to an absolute ratio\n     * @param {number} ratio - The target ratio.\n     * @param {Object} pivot - The zoom pivot point coordinate.\n     * @param {Event} _originalEvent - The original event if any.\n     * @returns {Cropper} this\n     */\n    zoomTo: function zoomTo(ratio, pivot, _originalEvent) {\n      var options = this.options,\n          canvasData = this.canvasData;\n      var width = canvasData.width,\n          height = canvasData.height,\n          naturalWidth = canvasData.naturalWidth,\n          naturalHeight = canvasData.naturalHeight;\n      ratio = Number(ratio);\n\n      if (ratio >= 0 && this.ready && !this.disabled && options.zoomable) {\n        var newWidth = naturalWidth * ratio;\n        var newHeight = naturalHeight * ratio;\n\n        if (dispatchEvent(this.element, EVENT_ZOOM, {\n          ratio: ratio,\n          oldRatio: width / naturalWidth,\n          originalEvent: _originalEvent\n        }) === false) {\n          return this;\n        }\n\n        if (_originalEvent) {\n          var pointers = this.pointers;\n          var offset = getOffset(this.cropper);\n          var center = pointers && Object.keys(pointers).length ? getPointersCenter(pointers) : {\n            pageX: _originalEvent.pageX,\n            pageY: _originalEvent.pageY\n          }; // Zoom from the triggering point of the event\n\n          canvasData.left -= (newWidth - width) * ((center.pageX - offset.left - canvasData.left) / width);\n          canvasData.top -= (newHeight - height) * ((center.pageY - offset.top - canvasData.top) / height);\n        } else if (isPlainObject(pivot) && isNumber(pivot.x) && isNumber(pivot.y)) {\n          canvasData.left -= (newWidth - width) * ((pivot.x - canvasData.left) / width);\n          canvasData.top -= (newHeight - height) * ((pivot.y - canvasData.top) / height);\n        } else {\n          // Zoom from the center of the canvas\n          canvasData.left -= (newWidth - width) / 2;\n          canvasData.top -= (newHeight - height) / 2;\n        }\n\n        canvasData.width = newWidth;\n        canvasData.height = newHeight;\n        this.renderCanvas(true);\n      }\n\n      return this;\n    },\n\n    /**\n     * Rotate the canvas with a relative degree\n     * @param {number} degree - The rotate degree.\n     * @returns {Cropper} this\n     */\n    rotate: function rotate(degree) {\n      return this.rotateTo((this.imageData.rotate || 0) + Number(degree));\n    },\n\n    /**\n     * Rotate the canvas to an absolute degree\n     * @param {number} degree - The rotate degree.\n     * @returns {Cropper} this\n     */\n    rotateTo: function rotateTo(degree) {\n      degree = Number(degree);\n\n      if (isNumber(degree) && this.ready && !this.disabled && this.options.rotatable) {\n        this.imageData.rotate = degree % 360;\n        this.renderCanvas(true, true);\n      }\n\n      return this;\n    },\n\n    /**\n     * Scale the image on the x-axis.\n     * @param {number} scaleX - The scale ratio on the x-axis.\n     * @returns {Cropper} this\n     */\n    scaleX: function scaleX(_scaleX) {\n      var scaleY = this.imageData.scaleY;\n      return this.scale(_scaleX, isNumber(scaleY) ? scaleY : 1);\n    },\n\n    /**\n     * Scale the image on the y-axis.\n     * @param {number} scaleY - The scale ratio on the y-axis.\n     * @returns {Cropper} this\n     */\n    scaleY: function scaleY(_scaleY) {\n      var scaleX = this.imageData.scaleX;\n      return this.scale(isNumber(scaleX) ? scaleX : 1, _scaleY);\n    },\n\n    /**\n     * Scale the image\n     * @param {number} scaleX - The scale ratio on the x-axis.\n     * @param {number} [scaleY=scaleX] - The scale ratio on the y-axis.\n     * @returns {Cropper} this\n     */\n    scale: function scale(scaleX) {\n      var scaleY = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : scaleX;\n      var imageData = this.imageData;\n      var transformed = false;\n      scaleX = Number(scaleX);\n      scaleY = Number(scaleY);\n\n      if (this.ready && !this.disabled && this.options.scalable) {\n        if (isNumber(scaleX)) {\n          imageData.scaleX = scaleX;\n          transformed = true;\n        }\n\n        if (isNumber(scaleY)) {\n          imageData.scaleY = scaleY;\n          transformed = true;\n        }\n\n        if (transformed) {\n          this.renderCanvas(true, true);\n        }\n      }\n\n      return this;\n    },\n\n    /**\n     * Get the cropped area position and size data (base on the original image)\n     * @param {boolean} [rounded=false] - Indicate if round the data values or not.\n     * @returns {Object} The result cropped data.\n     */\n    getData: function getData() {\n      var rounded = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;\n      var options = this.options,\n          imageData = this.imageData,\n          canvasData = this.canvasData,\n          cropBoxData = this.cropBoxData;\n      var data;\n\n      if (this.ready && this.cropped) {\n        data = {\n          x: cropBoxData.left - canvasData.left,\n          y: cropBoxData.top - canvasData.top,\n          width: cropBoxData.width,\n          height: cropBoxData.height\n        };\n        var ratio = imageData.width / imageData.naturalWidth;\n        forEach(data, function (n, i) {\n          data[i] = n / ratio;\n        });\n\n        if (rounded) {\n          // In case rounding off leads to extra 1px in right or bottom border\n          // we should round the top-left corner and the dimension (#343).\n          var bottom = Math.round(data.y + data.height);\n          var right = Math.round(data.x + data.width);\n          data.x = Math.round(data.x);\n          data.y = Math.round(data.y);\n          data.width = right - data.x;\n          data.height = bottom - data.y;\n        }\n      } else {\n        data = {\n          x: 0,\n          y: 0,\n          width: 0,\n          height: 0\n        };\n      }\n\n      if (options.rotatable) {\n        data.rotate = imageData.rotate || 0;\n      }\n\n      if (options.scalable) {\n        data.scaleX = imageData.scaleX || 1;\n        data.scaleY = imageData.scaleY || 1;\n      }\n\n      return data;\n    },\n\n    /**\n     * Set the cropped area position and size with new data\n     * @param {Object} data - The new data.\n     * @returns {Cropper} this\n     */\n    setData: function setData(data) {\n      var options = this.options,\n          imageData = this.imageData,\n          canvasData = this.canvasData;\n      var cropBoxData = {};\n\n      if (this.ready && !this.disabled && isPlainObject(data)) {\n        var transformed = false;\n\n        if (options.rotatable) {\n          if (isNumber(data.rotate) && data.rotate !== imageData.rotate) {\n            imageData.rotate = data.rotate;\n            transformed = true;\n          }\n        }\n\n        if (options.scalable) {\n          if (isNumber(data.scaleX) && data.scaleX !== imageData.scaleX) {\n            imageData.scaleX = data.scaleX;\n            transformed = true;\n          }\n\n          if (isNumber(data.scaleY) && data.scaleY !== imageData.scaleY) {\n            imageData.scaleY = data.scaleY;\n            transformed = true;\n          }\n        }\n\n        if (transformed) {\n          this.renderCanvas(true, true);\n        }\n\n        var ratio = imageData.width / imageData.naturalWidth;\n\n        if (isNumber(data.x)) {\n          cropBoxData.left = data.x * ratio + canvasData.left;\n        }\n\n        if (isNumber(data.y)) {\n          cropBoxData.top = data.y * ratio + canvasData.top;\n        }\n\n        if (isNumber(data.width)) {\n          cropBoxData.width = data.width * ratio;\n        }\n\n        if (isNumber(data.height)) {\n          cropBoxData.height = data.height * ratio;\n        }\n\n        this.setCropBoxData(cropBoxData);\n      }\n\n      return this;\n    },\n\n    /**\n     * Get the container size data.\n     * @returns {Object} The result container data.\n     */\n    getContainerData: function getContainerData() {\n      return this.ready ? assign({}, this.containerData) : {};\n    },\n\n    /**\n     * Get the image position and size data.\n     * @returns {Object} The result image data.\n     */\n    getImageData: function getImageData() {\n      return this.sized ? assign({}, this.imageData) : {};\n    },\n\n    /**\n     * Get the canvas position and size data.\n     * @returns {Object} The result canvas data.\n     */\n    getCanvasData: function getCanvasData() {\n      var canvasData = this.canvasData;\n      var data = {};\n\n      if (this.ready) {\n        forEach(['left', 'top', 'width', 'height', 'naturalWidth', 'naturalHeight'], function (n) {\n          data[n] = canvasData[n];\n        });\n      }\n\n      return data;\n    },\n\n    /**\n     * Set the canvas position and size with new data.\n     * @param {Object} data - The new canvas data.\n     * @returns {Cropper} this\n     */\n    setCanvasData: function setCanvasData(data) {\n      var canvasData = this.canvasData;\n      var aspectRatio = canvasData.aspectRatio;\n\n      if (this.ready && !this.disabled && isPlainObject(data)) {\n        if (isNumber(data.left)) {\n          canvasData.left = data.left;\n        }\n\n        if (isNumber(data.top)) {\n          canvasData.top = data.top;\n        }\n\n        if (isNumber(data.width)) {\n          canvasData.width = data.width;\n          canvasData.height = data.width / aspectRatio;\n        } else if (isNumber(data.height)) {\n          canvasData.height = data.height;\n          canvasData.width = data.height * aspectRatio;\n        }\n\n        this.renderCanvas(true);\n      }\n\n      return this;\n    },\n\n    /**\n     * Get the crop box position and size data.\n     * @returns {Object} The result crop box data.\n     */\n    getCropBoxData: function getCropBoxData() {\n      var cropBoxData = this.cropBoxData;\n      var data;\n\n      if (this.ready && this.cropped) {\n        data = {\n          left: cropBoxData.left,\n          top: cropBoxData.top,\n          width: cropBoxData.width,\n          height: cropBoxData.height\n        };\n      }\n\n      return data || {};\n    },\n\n    /**\n     * Set the crop box position and size with new data.\n     * @param {Object} data - The new crop box data.\n     * @returns {Cropper} this\n     */\n    setCropBoxData: function setCropBoxData(data) {\n      var cropBoxData = this.cropBoxData;\n      var aspectRatio = this.options.aspectRatio;\n      var widthChanged;\n      var heightChanged;\n\n      if (this.ready && this.cropped && !this.disabled && isPlainObject(data)) {\n        if (isNumber(data.left)) {\n          cropBoxData.left = data.left;\n        }\n\n        if (isNumber(data.top)) {\n          cropBoxData.top = data.top;\n        }\n\n        if (isNumber(data.width) && data.width !== cropBoxData.width) {\n          widthChanged = true;\n          cropBoxData.width = data.width;\n        }\n\n        if (isNumber(data.height) && data.height !== cropBoxData.height) {\n          heightChanged = true;\n          cropBoxData.height = data.height;\n        }\n\n        if (aspectRatio) {\n          if (widthChanged) {\n            cropBoxData.height = cropBoxData.width / aspectRatio;\n          } else if (heightChanged) {\n            cropBoxData.width = cropBoxData.height * aspectRatio;\n          }\n        }\n\n        this.renderCropBox();\n      }\n\n      return this;\n    },\n\n    /**\n     * Get a canvas drawn the cropped image.\n     * @param {Object} [options={}] - The config options.\n     * @returns {HTMLCanvasElement} - The result canvas.\n     */\n    getCroppedCanvas: function getCroppedCanvas() {\n      var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n      if (!this.ready || !window.HTMLCanvasElement) {\n        return null;\n      }\n\n      var canvasData = this.canvasData;\n      var source = getSourceCanvas(this.image, this.imageData, canvasData, options); // Returns the source canvas if it is not cropped.\n\n      if (!this.cropped) {\n        return source;\n      }\n\n      var _this$getData = this.getData(),\n          initialX = _this$getData.x,\n          initialY = _this$getData.y,\n          initialWidth = _this$getData.width,\n          initialHeight = _this$getData.height;\n\n      var ratio = source.width / Math.floor(canvasData.naturalWidth);\n\n      if (ratio !== 1) {\n        initialX *= ratio;\n        initialY *= ratio;\n        initialWidth *= ratio;\n        initialHeight *= ratio;\n      }\n\n      var aspectRatio = initialWidth / initialHeight;\n      var maxSizes = getAdjustedSizes({\n        aspectRatio: aspectRatio,\n        width: options.maxWidth || Infinity,\n        height: options.maxHeight || Infinity\n      });\n      var minSizes = getAdjustedSizes({\n        aspectRatio: aspectRatio,\n        width: options.minWidth || 0,\n        height: options.minHeight || 0\n      }, 'cover');\n\n      var _getAdjustedSizes = getAdjustedSizes({\n        aspectRatio: aspectRatio,\n        width: options.width || (ratio !== 1 ? source.width : initialWidth),\n        height: options.height || (ratio !== 1 ? source.height : initialHeight)\n      }),\n          width = _getAdjustedSizes.width,\n          height = _getAdjustedSizes.height;\n\n      width = Math.min(maxSizes.width, Math.max(minSizes.width, width));\n      height = Math.min(maxSizes.height, Math.max(minSizes.height, height));\n      var canvas = document.createElement('canvas');\n      var context = canvas.getContext('2d');\n      canvas.width = normalizeDecimalNumber(width);\n      canvas.height = normalizeDecimalNumber(height);\n      context.fillStyle = options.fillColor || 'transparent';\n      context.fillRect(0, 0, width, height);\n      var _options$imageSmoothi = options.imageSmoothingEnabled,\n          imageSmoothingEnabled = _options$imageSmoothi === void 0 ? true : _options$imageSmoothi,\n          imageSmoothingQuality = options.imageSmoothingQuality;\n      context.imageSmoothingEnabled = imageSmoothingEnabled;\n\n      if (imageSmoothingQuality) {\n        context.imageSmoothingQuality = imageSmoothingQuality;\n      } // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D.drawImage\n\n\n      var sourceWidth = source.width;\n      var sourceHeight = source.height; // Source canvas parameters\n\n      var srcX = initialX;\n      var srcY = initialY;\n      var srcWidth;\n      var srcHeight; // Destination canvas parameters\n\n      var dstX;\n      var dstY;\n      var dstWidth;\n      var dstHeight;\n\n      if (srcX <= -initialWidth || srcX > sourceWidth) {\n        srcX = 0;\n        srcWidth = 0;\n        dstX = 0;\n        dstWidth = 0;\n      } else if (srcX <= 0) {\n        dstX = -srcX;\n        srcX = 0;\n        srcWidth = Math.min(sourceWidth, initialWidth + srcX);\n        dstWidth = srcWidth;\n      } else if (srcX <= sourceWidth) {\n        dstX = 0;\n        srcWidth = Math.min(initialWidth, sourceWidth - srcX);\n        dstWidth = srcWidth;\n      }\n\n      if (srcWidth <= 0 || srcY <= -initialHeight || srcY > sourceHeight) {\n        srcY = 0;\n        srcHeight = 0;\n        dstY = 0;\n        dstHeight = 0;\n      } else if (srcY <= 0) {\n        dstY = -srcY;\n        srcY = 0;\n        srcHeight = Math.min(sourceHeight, initialHeight + srcY);\n        dstHeight = srcHeight;\n      } else if (srcY <= sourceHeight) {\n        dstY = 0;\n        srcHeight = Math.min(initialHeight, sourceHeight - srcY);\n        dstHeight = srcHeight;\n      }\n\n      var params = [srcX, srcY, srcWidth, srcHeight]; // Avoid \"IndexSizeError\"\n\n      if (dstWidth > 0 && dstHeight > 0) {\n        var scale = width / initialWidth;\n        params.push(dstX * scale, dstY * scale, dstWidth * scale, dstHeight * scale);\n      } // All the numerical parameters should be integer for `drawImage`\n      // https://github.com/fengyuanchen/cropper/issues/476\n\n\n      context.drawImage.apply(context, [source].concat(_toConsumableArray(params.map(function (param) {\n        return Math.floor(normalizeDecimalNumber(param));\n      }))));\n      return canvas;\n    },\n\n    /**\n     * Change the aspect ratio of the crop box.\n     * @param {number} aspectRatio - The new aspect ratio.\n     * @returns {Cropper} this\n     */\n    setAspectRatio: function setAspectRatio(aspectRatio) {\n      var options = this.options;\n\n      if (!this.disabled && !isUndefined(aspectRatio)) {\n        // 0 -> NaN\n        options.aspectRatio = Math.max(0, aspectRatio) || NaN;\n\n        if (this.ready) {\n          this.initCropBox();\n\n          if (this.cropped) {\n            this.renderCropBox();\n          }\n        }\n      }\n\n      return this;\n    },\n\n    /**\n     * Change the drag mode.\n     * @param {string} mode - The new drag mode.\n     * @returns {Cropper} this\n     */\n    setDragMode: function setDragMode(mode) {\n      var options = this.options,\n          dragBox = this.dragBox,\n          face = this.face;\n\n      if (this.ready && !this.disabled) {\n        var croppable = mode === DRAG_MODE_CROP;\n        var movable = options.movable && mode === DRAG_MODE_MOVE;\n        mode = croppable || movable ? mode : DRAG_MODE_NONE;\n        options.dragMode = mode;\n        setData(dragBox, DATA_ACTION, mode);\n        toggleClass(dragBox, CLASS_CROP, croppable);\n        toggleClass(dragBox, CLASS_MOVE, movable);\n\n        if (!options.cropBoxMovable) {\n          // Sync drag mode to crop box when it is not movable\n          setData(face, DATA_ACTION, mode);\n          toggleClass(face, CLASS_CROP, croppable);\n          toggleClass(face, CLASS_MOVE, movable);\n        }\n      }\n\n      return this;\n    }\n  };\n\n  var AnotherCropper = WINDOW.Cropper;\n\n  var Cropper =\n  /*#__PURE__*/\n  function () {\n    /**\n     * Create a new Cropper.\n     * @param {Element} element - The target element for cropping.\n     * @param {Object} [options={}] - The configuration options.\n     */\n    function Cropper(element) {\n      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n      _classCallCheck(this, Cropper);\n\n      if (!element || !REGEXP_TAG_NAME.test(element.tagName)) {\n        throw new Error('The first argument is required and must be an <img> or <canvas> element.');\n      }\n\n      this.element = element;\n      this.options = assign({}, DEFAULTS, isPlainObject(options) && options);\n      this.cropped = false;\n      this.disabled = false;\n      this.pointers = {};\n      this.ready = false;\n      this.reloading = false;\n      this.replaced = false;\n      this.sized = false;\n      this.sizing = false;\n      this.init();\n    }\n\n    _createClass(Cropper, [{\n      key: \"init\",\n      value: function init() {\n        var element = this.element;\n        var tagName = element.tagName.toLowerCase();\n        var url;\n\n        if (element[NAMESPACE]) {\n          return;\n        }\n\n        element[NAMESPACE] = this;\n\n        if (tagName === 'img') {\n          this.isImg = true; // e.g.: \"img/picture.jpg\"\n\n          url = element.getAttribute('src') || '';\n          this.originalUrl = url; // Stop when it's a blank image\n\n          if (!url) {\n            return;\n          } // e.g.: \"http://example.com/img/picture.jpg\"\n\n\n          url = element.src;\n        } else if (tagName === 'canvas' && window.HTMLCanvasElement) {\n          url = element.toDataURL();\n        }\n\n        this.load(url);\n      }\n    }, {\n      key: \"load\",\n      value: function load(url) {\n        var _this = this;\n\n        if (!url) {\n          return;\n        }\n\n        this.url = url;\n        this.imageData = {};\n        var element = this.element,\n            options = this.options;\n\n        if (!options.rotatable && !options.scalable) {\n          options.checkOrientation = false;\n        } // Only IE10+ supports Typed Arrays\n\n\n        if (!options.checkOrientation || !window.ArrayBuffer) {\n          this.clone();\n          return;\n        } // Detect the mime type of the image directly if it is a Data URL\n\n\n        if (REGEXP_DATA_URL.test(url)) {\n          // Read ArrayBuffer from Data URL of JPEG images directly for better performance\n          if (REGEXP_DATA_URL_JPEG.test(url)) {\n            this.read(dataURLToArrayBuffer(url));\n          } else {\n            // Only a JPEG image may contains Exif Orientation information,\n            // the rest types of Data URLs are not necessary to check orientation at all.\n            this.clone();\n          }\n\n          return;\n        } // 1. Detect the mime type of the image by a XMLHttpRequest.\n        // 2. Load the image as ArrayBuffer for reading orientation if its a JPEG image.\n\n\n        var xhr = new XMLHttpRequest();\n        var clone = this.clone.bind(this);\n        this.reloading = true;\n        this.xhr = xhr; // 1. Cross origin requests are only supported for protocol schemes:\n        // http, https, data, chrome, chrome-extension.\n        // 2. Access to XMLHttpRequest from a Data URL will be blocked by CORS policy\n        // in some browsers as IE11 and Safari.\n\n        xhr.onabort = clone;\n        xhr.onerror = clone;\n        xhr.ontimeout = clone;\n\n        xhr.onprogress = function () {\n          // Abort the request directly if it not a JPEG image for better performance\n          if (xhr.getResponseHeader('content-type') !== MIME_TYPE_JPEG) {\n            xhr.abort();\n          }\n        };\n\n        xhr.onload = function () {\n          _this.read(xhr.response);\n        };\n\n        xhr.onloadend = function () {\n          _this.reloading = false;\n          _this.xhr = null;\n        }; // Bust cache when there is a \"crossOrigin\" property to avoid browser cache error\n\n\n        if (options.checkCrossOrigin && isCrossOriginURL(url) && element.crossOrigin) {\n          url = addTimestamp(url);\n        }\n\n        xhr.open('GET', url);\n        xhr.responseType = 'arraybuffer';\n        xhr.withCredentials = element.crossOrigin === 'use-credentials';\n        xhr.send();\n      }\n    }, {\n      key: \"read\",\n      value: function read(arrayBuffer) {\n        var options = this.options,\n            imageData = this.imageData; // Reset the orientation value to its default value 1\n        // as some iOS browsers will render image with its orientation\n\n        var orientation = resetAndGetOrientation(arrayBuffer);\n        var rotate = 0;\n        var scaleX = 1;\n        var scaleY = 1;\n\n        if (orientation > 1) {\n          // Generate a new URL which has the default orientation value\n          this.url = arrayBufferToDataURL(arrayBuffer, MIME_TYPE_JPEG);\n\n          var _parseOrientation = parseOrientation(orientation);\n\n          rotate = _parseOrientation.rotate;\n          scaleX = _parseOrientation.scaleX;\n          scaleY = _parseOrientation.scaleY;\n        }\n\n        if (options.rotatable) {\n          imageData.rotate = rotate;\n        }\n\n        if (options.scalable) {\n          imageData.scaleX = scaleX;\n          imageData.scaleY = scaleY;\n        }\n\n        this.clone();\n      }\n    }, {\n      key: \"clone\",\n      value: function clone() {\n        var element = this.element,\n            url = this.url;\n        var crossOrigin = element.crossOrigin;\n        var crossOriginUrl = url;\n\n        if (this.options.checkCrossOrigin && isCrossOriginURL(url)) {\n          if (!crossOrigin) {\n            crossOrigin = 'anonymous';\n          } // Bust cache when there is not a \"crossOrigin\" property (#519)\n\n\n          crossOriginUrl = addTimestamp(url);\n        }\n\n        this.crossOrigin = crossOrigin;\n        this.crossOriginUrl = crossOriginUrl;\n        var image = document.createElement('img');\n\n        if (crossOrigin) {\n          image.crossOrigin = crossOrigin;\n        }\n\n        image.src = crossOriginUrl || url;\n        image.alt = element.alt || 'The image to crop';\n        this.image = image;\n        image.onload = this.start.bind(this);\n        image.onerror = this.stop.bind(this);\n        addClass(image, CLASS_HIDE);\n        element.parentNode.insertBefore(image, element.nextSibling);\n      }\n    }, {\n      key: \"start\",\n      value: function start() {\n        var _this2 = this;\n\n        var image = this.image;\n        image.onload = null;\n        image.onerror = null;\n        this.sizing = true; // Match all browsers that use WebKit as the layout engine in iOS devices,\n        // such as Safari for iOS, Chrome for iOS, and in-app browsers.\n\n        var isIOSWebKit = WINDOW.navigator && /(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(WINDOW.navigator.userAgent);\n\n        var done = function done(naturalWidth, naturalHeight) {\n          assign(_this2.imageData, {\n            naturalWidth: naturalWidth,\n            naturalHeight: naturalHeight,\n            aspectRatio: naturalWidth / naturalHeight\n          });\n          _this2.sizing = false;\n          _this2.sized = true;\n\n          _this2.build();\n        }; // Most modern browsers (excepts iOS WebKit)\n\n\n        if (image.naturalWidth && !isIOSWebKit) {\n          done(image.naturalWidth, image.naturalHeight);\n          return;\n        }\n\n        var sizingImage = document.createElement('img');\n        var body = document.body || document.documentElement;\n        this.sizingImage = sizingImage;\n\n        sizingImage.onload = function () {\n          done(sizingImage.width, sizingImage.height);\n\n          if (!isIOSWebKit) {\n            body.removeChild(sizingImage);\n          }\n        };\n\n        sizingImage.src = image.src; // iOS WebKit will convert the image automatically\n        // with its orientation once append it into DOM (#279)\n\n        if (!isIOSWebKit) {\n          sizingImage.style.cssText = 'left:0;' + 'max-height:none!important;' + 'max-width:none!important;' + 'min-height:0!important;' + 'min-width:0!important;' + 'opacity:0;' + 'position:absolute;' + 'top:0;' + 'z-index:-1;';\n          body.appendChild(sizingImage);\n        }\n      }\n    }, {\n      key: \"stop\",\n      value: function stop() {\n        var image = this.image;\n        image.onload = null;\n        image.onerror = null;\n        image.parentNode.removeChild(image);\n        this.image = null;\n      }\n    }, {\n      key: \"build\",\n      value: function build() {\n        if (!this.sized || this.ready) {\n          return;\n        }\n\n        var element = this.element,\n            options = this.options,\n            image = this.image; // Create cropper elements\n\n        var container = element.parentNode;\n        var template = document.createElement('div');\n        template.innerHTML = TEMPLATE;\n        var cropper = template.querySelector(\".\".concat(NAMESPACE, \"-container\"));\n        var canvas = cropper.querySelector(\".\".concat(NAMESPACE, \"-canvas\"));\n        var dragBox = cropper.querySelector(\".\".concat(NAMESPACE, \"-drag-box\"));\n        var cropBox = cropper.querySelector(\".\".concat(NAMESPACE, \"-crop-box\"));\n        var face = cropBox.querySelector(\".\".concat(NAMESPACE, \"-face\"));\n        this.container = container;\n        this.cropper = cropper;\n        this.canvas = canvas;\n        this.dragBox = dragBox;\n        this.cropBox = cropBox;\n        this.viewBox = cropper.querySelector(\".\".concat(NAMESPACE, \"-view-box\"));\n        this.face = face;\n        canvas.appendChild(image); // Hide the original image\n\n        addClass(element, CLASS_HIDDEN); // Inserts the cropper after to the current image\n\n        container.insertBefore(cropper, element.nextSibling); // Show the image if is hidden\n\n        if (!this.isImg) {\n          removeClass(image, CLASS_HIDE);\n        }\n\n        this.initPreview();\n        this.bind();\n        options.initialAspectRatio = Math.max(0, options.initialAspectRatio) || NaN;\n        options.aspectRatio = Math.max(0, options.aspectRatio) || NaN;\n        options.viewMode = Math.max(0, Math.min(3, Math.round(options.viewMode))) || 0;\n        addClass(cropBox, CLASS_HIDDEN);\n\n        if (!options.guides) {\n          addClass(cropBox.getElementsByClassName(\"\".concat(NAMESPACE, \"-dashed\")), CLASS_HIDDEN);\n        }\n\n        if (!options.center) {\n          addClass(cropBox.getElementsByClassName(\"\".concat(NAMESPACE, \"-center\")), CLASS_HIDDEN);\n        }\n\n        if (options.background) {\n          addClass(cropper, \"\".concat(NAMESPACE, \"-bg\"));\n        }\n\n        if (!options.highlight) {\n          addClass(face, CLASS_INVISIBLE);\n        }\n\n        if (options.cropBoxMovable) {\n          addClass(face, CLASS_MOVE);\n          setData(face, DATA_ACTION, ACTION_ALL);\n        }\n\n        if (!options.cropBoxResizable) {\n          addClass(cropBox.getElementsByClassName(\"\".concat(NAMESPACE, \"-line\")), CLASS_HIDDEN);\n          addClass(cropBox.getElementsByClassName(\"\".concat(NAMESPACE, \"-point\")), CLASS_HIDDEN);\n        }\n\n        this.render();\n        this.ready = true;\n        this.setDragMode(options.dragMode);\n\n        if (options.autoCrop) {\n          this.crop();\n        }\n\n        this.setData(options.data);\n\n        if (isFunction(options.ready)) {\n          addListener(element, EVENT_READY, options.ready, {\n            once: true\n          });\n        }\n\n        dispatchEvent(element, EVENT_READY);\n      }\n    }, {\n      key: \"unbuild\",\n      value: function unbuild() {\n        if (!this.ready) {\n          return;\n        }\n\n        this.ready = false;\n        this.unbind();\n        this.resetPreview();\n        this.cropper.parentNode.removeChild(this.cropper);\n        removeClass(this.element, CLASS_HIDDEN);\n      }\n    }, {\n      key: \"uncreate\",\n      value: function uncreate() {\n        if (this.ready) {\n          this.unbuild();\n          this.ready = false;\n          this.cropped = false;\n        } else if (this.sizing) {\n          this.sizingImage.onload = null;\n          this.sizing = false;\n          this.sized = false;\n        } else if (this.reloading) {\n          this.xhr.onabort = null;\n          this.xhr.abort();\n        } else if (this.image) {\n          this.stop();\n        }\n      }\n      /**\n       * Get the no conflict cropper class.\n       * @returns {Cropper} The cropper class.\n       */\n\n    }], [{\n      key: \"noConflict\",\n      value: function noConflict() {\n        window.Cropper = AnotherCropper;\n        return Cropper;\n      }\n      /**\n       * Change the default options.\n       * @param {Object} options - The new default options.\n       */\n\n    }, {\n      key: \"setDefaults\",\n      value: function setDefaults(options) {\n        assign(DEFAULTS, isPlainObject(options) && options);\n      }\n    }]);\n\n    return Cropper;\n  }();\n\n  assign(Cropper.prototype, render, preview, events, handlers, change, methods);\n\n  return Cropper;\n\n}));\n", "/*!\n * jQuery Cropper v1.0.0\n * https://github.com/fengyuanchen/jquery-cropper\n *\n * Copyright (c) 2018 Chen Fengyuan\n * Released under the MIT license\n *\n * Date: 2018-04-01T06:20:13.168Z\n */\n\n(function (global, factory) {\n    typeof exports === 'object' && typeof module !== 'undefined' ? factory(require('jquery'), require('cropperjs')) :\n    typeof define === 'function' && define.amd ? define(['jquery', 'cropperjs'], factory) :\n    (factory(global.jQuery,global.Cropper));\n  }(this, (function ($,Cropper) { 'use strict';\n\n    $ = $ && $.hasOwnProperty('default') ? $['default'] : $;\n    Cropper = Cropper && Cropper.hasOwnProperty('default') ? Cropper['default'] : Cropper;\n\n    if ($.fn) {\n      var AnotherCropper = $.fn.cropper;\n      var NAMESPACE = 'cropper';\n\n      $.fn.cropper = function jQueryCropper(option) {\n        for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {\n          args[_key - 1] = arguments[_key];\n        }\n\n        var result = void 0;\n\n        this.each(function (i, element) {\n          var $element = $(element);\n          var isDestroy = option === 'destroy';\n          var cropper = $element.data(NAMESPACE);\n\n          if (!cropper) {\n            if (isDestroy) {\n              return;\n            }\n\n            var options = $.extend({}, $element.data(), $.isPlainObject(option) && option);\n\n            cropper = new Cropper(element, options);\n            $element.data(NAMESPACE, cropper);\n          }\n\n          if (typeof option === 'string') {\n            var fn = cropper[option];\n\n            if ($.isFunction(fn)) {\n              result = fn.apply(cropper, args);\n\n              if (result === cropper) {\n                result = undefined;\n              }\n\n              if (isDestroy) {\n                $element.removeData(NAMESPACE);\n              }\n            }\n          }\n        });\n\n        return result !== undefined ? result : this;\n      };\n\n      $.fn.cropper.Constructor = Cropper;\n      $.fn.cropper.setDefaults = Cropper.setDefaults;\n      $.fn.cropper.noConflict = function noConflict() {\n        $.fn.cropper = AnotherCropper;\n        return this;\n      };\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\n(function($){\n    'use strict';\n        var rad = Math.PI/180;\n\n        // public methods\n        var methods = {\n                init : function(settings) {\n                    return this.each(function() {\n                        var $this = $(this), transfo = $this.data('transfo');\n                        if (!transfo) {\n                            _init($this, settings);\n                        } else {\n                            _overwriteOptions($this, transfo, settings);\n                            _targetCss($this, transfo);\n                        }\n                    });\n                },\n\n                destroy : function() {\n                    return this.each(function() {\n                        var $this = $(this);\n                        if ($this.data('transfo')) {\n                            _destroy($this);\n                        }\n                    });\n                },\n\n                reset : function() {\n                    return this.each(function() {\n                        var $this = $(this);\n                        if ($this.data('transfo')) {\n                            _reset($this);\n                        }\n                    });\n                },\n\n                toggle : function() {\n                    return this.each(function() {\n                        var $this = $(this);\n                        var transfo = $this.data('transfo');\n                        if (transfo) {\n                            transfo.settings.hide = !transfo.settings.hide;\n                            _showHide($this, transfo);\n                        }\n                    });\n                },\n\n                hide : function() {\n                    return this.each(function() {\n                        var $this = $(this);\n                        var transfo = $this.data('transfo');\n                        if (transfo) {\n                            transfo.settings.hide = true;\n                            _showHide($this, transfo);\n                        }\n                    });\n                },\n\n                show : function() {\n                    return this.each(function() {\n                        var $this = $(this);\n                        var transfo = $this.data('transfo');\n                        if (transfo) {\n                            transfo.settings.hide = false;\n                            _showHide($this, transfo);\n                        }\n                    });\n                },\n\n                settings :  function() {\n                    if(this.length > 1) {\n                        this.map(function () {\n                            var $this = $(this);\n                            return $this.data('transfo') && $this.data('transfo').settings;\n                        });\n                    }\n                    return this.data('transfo') && $this.data('transfo').settings;\n                },\n                center :  function() {\n                    if(this.length > 1) {\n                        this.map(function () {\n                            var $this = $(this);\n                            return $this.data('transfo') && $this.data('transfo').$center.offset();\n                        });\n                    }\n                    return this.data('transfo') && this.data('transfo').$center.offset();\n                }\n        };\n\n        $.fn.transfo = function( method ) {\n            if ( methods[method] ) {\n                    return methods[method].apply( this, Array.prototype.slice.call( arguments, 1 ));\n            } else if ( typeof method === 'object' || ! method ) {\n                    return methods.init.apply( this, arguments );\n            } else {\n                    $.error( 'Method ' +  method + ' does not exist on jQuery.transfo' );\n            }\n            return false;\n        };\n\n        function _init ($this, settings) {\n            var transfo = {};\n            $this.data('transfo', transfo);\n            transfo.settings = settings;\n            transfo.settings.document = transfo.settings.document || document;\n\n            // generate all the controls markup\n            var css = \"box-sizing: border-box; position: absolute; background-color: #fff; border: 1px solid #ccc; width: 8px; height: 8px; margin-left: -4px; margin-top: -4px;\";\n            transfo.$markup = $(''\n                + '<div class=\"transfo-container\">'\n                +  '<div class=\"transfo-controls\">'\n                +   '<div style=\"cursor: crosshair; position: absolute; margin: -30px; top: 0; right: 0; padding: 1px 0 0 1px;\" class=\"transfo-rotator\">'\n                +    '<span class=\"fa-stack fa-lg\">'\n                +    '<i class=\"fa fa-circle fa-stack-2x\"></i>'\n                +    '<i class=\"fa fa-repeat fa-stack-1x fa-inverse\"></i>'\n                +    '</span>'\n                +   '</div>'\n                +   '<div style=\"' + css + 'top: 0%; left: 0%; cursor: nw-resize;\" class=\"transfo-scaler-tl\"></div>'\n                +   '<div style=\"' + css + 'top: 0%; left: 100%; cursor: ne-resize;\" class=\"transfo-scaler-tr\"></div>'\n                +   '<div style=\"' + css + 'top: 100%; left: 100%; cursor: se-resize;\" class=\"transfo-scaler-br\"></div>'\n                +   '<div style=\"' + css + 'top: 100%; left: 0%; cursor: sw-resize;\" class=\"transfo-scaler-bl\"></div>'\n                +   '<div style=\"' + css + 'top: 0%; left: 50%; cursor: n-resize;\" class=\"transfo-scaler-tc\"></div>'\n                +   '<div style=\"' + css + 'top: 100%; left: 50%; cursor: s-resize;\" class=\"transfo-scaler-bc\"></div>'\n                +   '<div style=\"' + css + 'top: 50%; left: 0%; cursor: w-resize;\" class=\"transfo-scaler-ml\"></div>'\n                +   '<div style=\"' + css + 'top: 50%; left: 100%; cursor: e-resize;\" class=\"transfo-scaler-mr\"></div>'\n                +   '<div style=\"' + css + 'border: 0; width: 0px; height: 0px; top: 50%; left: 50%;\" class=\"transfo-scaler-mc\"></div>'\n                +  '</div>'\n                + '</div>');\n            transfo.$center = transfo.$markup.find(\".transfo-scaler-mc\");\n\n            // init setting and get css to set wrap\n            _setOptions($this, transfo);\n            _overwriteOptions ($this, transfo, settings);\n\n            // append controls to container\n            $(transfo.settings.document.body).append(transfo.$markup);\n\n            // set transfo container and markup\n            setTimeout(function () {\n                _targetCss($this, transfo);\n            },0);\n\n            _bind($this, transfo);\n            \n            _targetCss($this, transfo);\n            _stop_animation($this[0]);\n        }\n\n        function _overwriteOptions ($this, transfo, settings) {\n            transfo.settings = $.extend(transfo.settings, settings || {});\n        }\n\n        function _stop_animation (target) {\n            target.style.webkitAnimationPlayState = \"paused\";\n            target.style.animationPlayState = \"paused\";\n            target.style.webkitTransition = \"none\";\n            target.style.transition = \"none\";\n        }\n\n        function _setOptions ($this, transfo) {\n            var style = $this.attr(\"style\") || \"\";\n            var transform = style.match(/transform\\s*:([^;]+)/) ? style.match(/transform\\s*:([^;]+)/)[1] : \"\";\n\n            transfo.settings = {};\n\n            transfo.settings.angle=      transform.indexOf('rotate') != -1 ? parseFloat(transform.match(/rotate\\(([^)]+)deg\\)/)[1]) : 0;\n            transfo.settings.scalex=     transform.indexOf('scaleX') != -1 ? parseFloat(transform.match(/scaleX\\(([^)]+)\\)/)[1]) : 1;\n            transfo.settings.scaley=     transform.indexOf('scaleY') != -1 ? parseFloat(transform.match(/scaleY\\(([^)]+)\\)/)[1]) : 1;\n\n            transfo.settings.style = style.replace(/[^;]*transform[^;]+/g, '').replace(/;+/g, ';');\n\n            $this.attr(\"style\", transfo.settings.style);\n            _stop_animation($this[0]);\n            transfo.settings.pos = $this.offset();\n\n            transfo.settings.height = $this.innerHeight();\n            transfo.settings.width = $this.innerWidth();\n\n            var translatex = transform.match(/translateX\\(([0-9.-]+)(%|px)\\)/);\n            var translatey = transform.match(/translateY\\(([0-9.-]+)(%|px)\\)/);\n            transfo.settings.translate = \"%\";\n\n            if (translatex && translatex[2] === \"%\") {\n                transfo.settings.translatexp = parseFloat(translatex[1]);\n                transfo.settings.translatex = transfo.settings.translatexp / 100 * transfo.settings.width;\n            } else {\n                transfo.settings.translatex = translatex ? parseFloat(translatex[1]) : 0;\n            }\n            if (translatey && translatey[2] === \"%\") {\n                transfo.settings.translateyp = parseFloat(translatey[1]);\n                transfo.settings.translatey = transfo.settings.translateyp / 100 * transfo.settings.height;\n            } else {\n                transfo.settings.translatey = translatey ? parseFloat(translatey[1]) : 0;\n            }\n\n            transfo.settings.css = window.getComputedStyle($this[0], null);\n\n            transfo.settings.rotationStep = 5;\n            transfo.settings.hide = false;\n            transfo.settings.callback = function () {};\n        }\n\n        function _bind ($this, transfo) {\n            function mousedown (event) {\n                _mouseDown($this, this, transfo, event);\n                $(transfo.settings.document).on(\"mousemove\", mousemove).on(\"mouseup\", mouseup);\n            }\n            function mousemove (event) {\n                _mouseMove($this, this, transfo, event);\n            }\n            function mouseup (event) {\n                _mouseUp($this, this, transfo, event);\n                $(transfo.settings.document).off(\"mousemove\", mousemove).off(\"mouseup\", mouseup);\n            }\n\n            transfo.$markup.off().on(\"mousedown\", mousedown);\n            transfo.$markup.find(\".transfo-controls >:not(.transfo-scaler-mc)\").off().on(\"mousedown\", mousedown);\n        }\n\n        function _mouseDown($this, div, transfo, event) {\n            event.preventDefault();\n            if (transfo.active || event.which !== 1) return;\n\n            var type = \"position\", $e = $(div);\n            if ($e.hasClass(\"transfo-rotator\")) type = \"rotator\";\n            else if ($e.hasClass(\"transfo-scaler-tl\")) type = \"tl\";\n            else if ($e.hasClass(\"transfo-scaler-tr\")) type = \"tr\";\n            else if ($e.hasClass(\"transfo-scaler-br\")) type = \"br\";\n            else if ($e.hasClass(\"transfo-scaler-bl\")) type = \"bl\";\n            else if ($e.hasClass(\"transfo-scaler-tc\")) type = \"tc\";\n            else if ($e.hasClass(\"transfo-scaler-bc\")) type = \"bc\";\n            else if ($e.hasClass(\"transfo-scaler-ml\")) type = \"ml\";\n            else if ($e.hasClass(\"transfo-scaler-mr\")) type = \"mr\";\n\n            transfo.active = {\n                \"type\": type,\n                \"scalex\": transfo.settings.scalex,\n                \"scaley\": transfo.settings.scaley,\n                \"pageX\": event.pageX,\n                \"pageY\": event.pageY,\n                \"center\": transfo.$center.offset(),\n            };\n        }\n        function _mouseUp($this, div, transfo, event) {\n            transfo.active = null;\n        }\n\n        function _mouseMove($this, div, transfo, event) {\n            event.preventDefault();\n            if (!transfo.active) return;\n            var settings = transfo.settings;\n            var center = transfo.active.center;\n            var cdx = center.left - event.pageX;\n            var cdy = center.top - event.pageY;\n\n            if (transfo.active.type == \"rotator\") {\n                var ang, dang = Math.atan((settings.width * settings.scalex) / (settings.height * settings.scaley)) / rad;\n\n                if (cdy) ang = Math.atan(- cdx / cdy) / rad;\n                else ang = 0;\n                if (event.pageY >= center.top && event.pageX >= center.left) ang += 180;\n                else if (event.pageY >= center.top && event.pageX < center.left) ang += 180;\n                else if (event.pageY < center.top && event.pageX < center.left) ang += 360;\n                \n                ang -= dang;\n                if (settings.scaley < 0 && settings.scalex < 0) ang += 180;\n\n                if (!event.ctrlKey) {\n                    settings.angle = Math.round(ang / transfo.settings.rotationStep) * transfo.settings.rotationStep;\n                } else {\n                    settings.angle = ang;\n                }\n\n                // reset position : don't move center\n                _targetCss($this, transfo);\n                var new_center = transfo.$center.offset();\n                var x = center.left - new_center.left;\n                var y = center.top - new_center.top;\n                var 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            }\n            else if (transfo.active.type == \"position\") {\n                var angle = settings.angle * rad;\n                var x = event.pageX - transfo.active.pageX;\n                var y = event.pageY - transfo.active.pageY;\n                transfo.active.pageX = event.pageX;\n                transfo.active.pageY = event.pageY;\n                var dx = x*Math.cos(angle) - y*Math.sin(-angle);\n                var dy = - x*Math.sin(angle) + y*Math.cos(-angle);\n\n                settings.translatex += dx;\n                settings.translatey += dy;\n            }\n            else if (transfo.active.type.length === 2) {\n                var angle = settings.angle * rad;\n                var dx =   cdx*Math.cos(angle) - cdy*Math.sin(-angle);\n                var dy = - cdx*Math.sin(angle) + cdy*Math.cos(-angle);\n                if (transfo.active.type.indexOf(\"t\") != -1) {\n                    settings.scaley = dy / (settings.height/2);\n                }\n                if (transfo.active.type.indexOf(\"b\") != -1) {\n                    settings.scaley = - dy / (settings.height/2);\n                }\n                if (transfo.active.type.indexOf(\"l\") != -1) {\n                    settings.scalex = dx / (settings.width/2);\n                }\n                if (transfo.active.type.indexOf(\"r\") != -1) {\n                    settings.scalex = - dx / (settings.width/2);\n                }\n                if (settings.scaley > 0 && settings.scaley < 0.05) settings.scaley = 0.05;\n                if (settings.scalex > 0 && settings.scalex < 0.05) settings.scalex = 0.05;\n                if (settings.scaley < 0 && settings.scaley > -0.05) settings.scaley = -0.05;\n                if (settings.scalex < 0 && settings.scalex > -0.05) settings.scalex = -0.05;\n\n                if (event.shiftKey &&\n                    (transfo.active.type === \"tl\" || transfo.active.type === \"bl\" ||\n                     transfo.active.type === \"tr\" || transfo.active.type === \"br\")) {\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\n            _targetCss($this, transfo);\n            _stop_animation($this[0]);\n            return false;\n        }\n\n        function _setCss($this, css, settings) {\n            var transform = \"\";\n            var trans = false;\n            if (settings.angle !== 0) {\n                trans = true;\n                transform += \" rotate(\"+settings.angle+\"deg) \";\n            }\n            if (settings.translatex) {\n                trans = true;\n                transform += \" translateX(\"+(settings.translate === \"%\" ? settings.translatexp+\"%\" : settings.translatex+\"px\")+\") \";\n            }\n            if (settings.translatey) {\n                trans = true;\n                transform += \" translateY(\"+(settings.translate === \"%\" ? settings.translateyp+\"%\" : settings.translatey+\"px\")+\") \";\n            }\n            if (settings.scalex != 1) {\n                trans = true;\n                transform += \" scaleX(\"+settings.scalex+\") \";\n            }\n            if (settings.scaley != 1){\n                trans = true;\n                transform += \" scaleY(\"+settings.scaley+\") \";\n            }\n\n            if (trans) {\n                css += \";\"\n                        /* Safari */\n                css += \"-webkit-transform:\" + transform + \";\"\n                        /* Firefox */\n                    + \"-moz-transform:\" + transform + \";\"\n                        /* IE */\n                    + \"-ms-transform:\" + transform + \";\"\n                        /* Opera */\n                    + \"-o-transform:\" + transform + \";\"\n                        /* Other */\n                    + \"transform:\" + transform + \";\";\n            }\n\n            css = css.replace(/(\\s*;)+/g, ';').replace(/^\\s*;|;\\s*$/g, '');\n\n            $this.attr(\"style\", css);\n        }\n\n        function _targetCss ($this, transfo) {\n            var settings = transfo.settings;\n            var width = parseFloat(settings.css.width);\n            var 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            _setCss($this, settings.style, settings);\n\n            transfo.$markup.css({\n                \"position\": \"absolute\",\n                \"width\": width + \"px\",\n                \"height\": height + \"px\",\n                \"top\": settings.pos.top + \"px\",\n                \"left\": settings.pos.left + \"px\"\n            });\n\n            var $controls = transfo.$markup.find('.transfo-controls');\n            _setCss($controls,\n                \"width:\" + width + \"px;\" +\n                \"height:\" + height + \"px;\" +\n                \"cursor: move;\",\n                settings);\n\n            $controls.children().css(\"transform\", \"scaleX(\"+(1/settings.scalex)+\") scaleY(\"+(1/settings.scaley)+\")\");\n\n            _showHide($this, transfo);\n\n            transfo.settings.callback.call($this[0], $this);\n        }\n\n        function _showHide ($this, transfo) {\n            transfo.$markup.css(\"z-index\", transfo.settings.hide ? -1 : 1000);\n            if (transfo.settings.hide) {\n                transfo.$markup.find(\".transfo-controls > *\").hide();\n                transfo.$markup.find(\".transfo-scaler-mc\").show();\n            } else {\n                transfo.$markup.find(\".transfo-controls > *\").show();\n            }\n        }\n\n        function _destroy ($this) {\n            $this.data('transfo').$markup.remove();\n            $this.removeData('transfo');\n        }\n\n        function _reset ($this) {\n            var transfo = $this.data('transfo');\n            _destroy($this);\n            $this.transfo(transfo.settings);\n        }\n\n})(jQuery);\n", "/* \nWebGLImageFilter - MIT Licensed\n\n2013, Dominic Szablewski - phoboslab.org\n*/\n\n(function(window){\n\nvar WebGLProgram = function( gl, vertexSource, fragmentSource ) {\n\n\tvar _collect = function( source, prefix, collection ) {\n\t\tvar r = new RegExp('\\\\b' + prefix + ' \\\\w+ (\\\\w+)', 'ig');\n\t\tsource.replace(r, function(match, name) {\n\t\t\tcollection[name] = 0;\n\t\t\treturn match;\n\t\t});\n\t};\n\n\tvar _compile = function( gl, source, type ) {\n\t\tvar shader = gl.createShader(type);\n\t\tgl.shaderSource(shader, source);\n\t\tgl.compileShader(shader);\n\n\t\tif( !gl.getShaderParameter(shader, gl.COMPILE_STATUS) ) {\n\t\t\tconsole.log(gl.getShaderInfoLog(shader));\n\t\t\treturn null;\n\t\t}\n\t\treturn shader;\n\t};\n\n\n\tthis.uniform = {};\n\tthis.attribute = {};\n\n\tvar _vsh = _compile(gl, vertexSource, gl.VERTEX_SHADER);\n\tvar _fsh = _compile(gl, fragmentSource, gl.FRAGMENT_SHADER);\n\n\tthis.id = gl.createProgram();\n\tgl.attachShader(this.id, _vsh);\n\tgl.attachShader(this.id, _fsh);\n\tgl.linkProgram(this.id);\n\n\tif( !gl.getProgramParameter(this.id, gl.LINK_STATUS) ) {\n\t\tconsole.log(gl.getProgramInfoLog(this.id));\n\t}\n\n\tgl.useProgram(this.id);\n\n\t// Collect attributes\n\t_collect(vertexSource, 'attribute', this.attribute);\n\tfor( var a in this.attribute ) {\n\t\tthis.attribute[a] = gl.getAttribLocation(this.id, a);\n\t}\n\n\t// Collect uniforms\n\t_collect(vertexSource, 'uniform', this.uniform);\n\t_collect(fragmentSource, 'uniform', this.uniform);\n\tfor( var u in this.uniform ) {\n\t\tthis.uniform[u] = gl.getUniformLocation(this.id, u);\n\t}\n};\n\nconst identityMatrix = [\n\t1, 0, 0, 0, 0,\n\t0, 1, 0, 0, 0,\n\t0, 0, 1, 0, 0,\n\t0, 0, 0, 1, 0,\n];\n\nconst weightedAvg = (a, b, w) => a * w + b * (1 - w);\n\nvar WebGLImageFilter = window.WebGLImageFilter = function (params) {\n\tif (!params)\n\t\tparams = { };\n\n\tvar \n\t\tgl = null,\n\t\t_drawCount = 0,\n\t\t_sourceTexture = null,\n\t\t_lastInChain = false,\n\t\t_currentFramebufferIndex = -1,\n\t\t_tempFramebuffers = [null, null],\n\t\t_filterChain = [],\n\t\t_width = -1, \n\t\t_height = -1,\n\t\t_vertexBuffer = null,\n\t\t_currentProgram = null,\n\t\t_canvas = params.canvas || document.createElement('canvas');\n\n\t// key is the shader program source, value is the compiled program\n\tvar _shaderProgramCache = { };\n\n\tvar gl = _canvas.getContext(\"webgl\") || _canvas.getContext(\"experimental-webgl\");\n\tif( !gl ) {\n\t\tthrow \"Couldn't get WebGL context\";\n\t}\n\n\t\n\tthis.addFilter = function( name ) {\n\t\tvar args = Array.prototype.slice.call(arguments, 1);\n\t\tvar filter = _filter[name];\n\n\t\t_filterChain.push({func:filter, args:args});\n\t};\n\n\tthis.reset = function() {\n\t\t_filterChain = [];\n\t};\n\t\n\tthis.apply = function( image ) {\n\t\t_resize( image.width, image.height );\n\t\t_drawCount = 0;\n\n\t\t// Create the texture for the input image if we haven't yet\n\t\tif (!_sourceTexture)\n\t\t\t_sourceTexture = gl.createTexture();\n\n\t\tgl.bindTexture(gl.TEXTURE_2D, _sourceTexture);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); \n\t\tgl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);\n\n\t\t// No filters? Just draw\n\t\tif( _filterChain.length == 0 ) {\n\t\t\tvar program = _compileShader(SHADER.FRAGMENT_IDENTITY);\n\t\t\t_draw();\n\t\t\treturn _canvas;\n\t\t}\n\n\t\tfor( var i = 0; i < _filterChain.length; i++ ) {\n\t\t\t_lastInChain = (i == _filterChain.length-1);\n\t\t\tvar f = _filterChain[i];\n\n\t\t\tf.func.apply(this, f.args || []);\n\t\t}\n\n\t\treturn _canvas;\n\t};\n\n\tvar _resize = function( width, height ) {\n\t\t// Same width/height? Nothing to do here\n\t\tif( width == _width && height == _height ) { return; }\n\n\n\t\t_canvas.width = _width = width;\n\t\t_canvas.height = _height = height;\n\n\t\t// Create the context if we don't have it yet\n\t\tif( !_vertexBuffer ) {\n\t\t\t// Create the vertex buffer for the two triangles [x, y, u, v] * 6\n\t\t\tvar vertices = new Float32Array([\n\t\t\t\t-1, -1, 0, 1,  1, -1, 1, 1,  -1, 1, 0, 0,\n\t\t\t\t-1, 1, 0, 0,  1, -1, 1, 1,  1, 1, 1, 0\n\t\t\t]);\n\t\t\t_vertexBuffer = gl.createBuffer(),\n\t\t\tgl.bindBuffer(gl.ARRAY_BUFFER, _vertexBuffer);\n\t\t\tgl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);\n\n\t\t\t// Note sure if this is a good idea; at least it makes texture loading\n\t\t\t// in Ejecta instant.\n\t\t\tgl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);\n\t\t}\n\n\t\tgl.viewport(0, 0, _width, _height);\n\n\t\t// Delete old temp framebuffers\n\t\t_tempFramebuffers = [null, null];\n\t};\n\n\tvar _getTempFramebuffer = function( index ) {\n\t\t_tempFramebuffers[index] = \n\t\t\t_tempFramebuffers[index] || \n\t\t\t_createFramebufferTexture( _width, _height );\n\n\t\treturn _tempFramebuffers[index];\n\t};\n\n\tvar _createFramebufferTexture = function( width, height ) {\n\t\tvar fbo = gl.createFramebuffer();\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, fbo);\n\n\t\tvar renderbuffer = gl.createRenderbuffer();\n\t\tgl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);\n\n\t\tvar texture = gl.createTexture();\n\t\tgl.bindTexture(gl.TEXTURE_2D, texture);\n\t\tgl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);\n\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n\t\tgl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n\n\t\tgl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);\n\n\t\tgl.bindTexture(gl.TEXTURE_2D, null);\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, null);\n\n\t\treturn {fbo: fbo, texture: texture};\n\t};\n\n\tvar _draw = function( flags ) {\n\t\tvar source = null, \n\t\t\ttarget = null,\n\t\t\tflipY = false;\n\n\t\t// Set up the source\n\t\tif( _drawCount == 0 ) {\n\t\t\t// First draw call - use the source texture\n\t\t\tsource = _sourceTexture;\n\t\t}\n\t\telse {\n\t\t\t// All following draw calls use the temp buffer last drawn to\n\t\t\tsource =  _getTempFramebuffer(_currentFramebufferIndex).texture;\n\t\t}\n\t\t_drawCount++;\n\n\n\t\t// Set up the target\n\t\tif( _lastInChain && !(flags & DRAW.INTERMEDIATE) ) {\n\t\t\t// Last filter in our chain - draw directly to the WebGL Canvas. We may\n\t\t\t// also have to flip the image vertically now\n\t\t\ttarget = null;\n\t\t\tflipY = _drawCount % 2 == 0;\n\t\t}\n\t\telse {\n\t\t\t// Intermediate draw call - get a temp buffer to draw to\n\t\t\t_currentFramebufferIndex = (_currentFramebufferIndex+1) % 2;\n\t\t\ttarget = _getTempFramebuffer(_currentFramebufferIndex).fbo;\n\t\t}\n\n\t\t// Bind the source and target and draw the two triangles\n\t\tgl.bindTexture(gl.TEXTURE_2D, source);\n\t\tgl.bindFramebuffer(gl.FRAMEBUFFER, target);\n\n\t\tgl.uniform1f(_currentProgram.uniform.flipY, (flipY ? -1 : 1) );\n\t\tgl.drawArrays(gl.TRIANGLES, 0, 6);\n\t};\n\n\tvar _compileShader = function( fragmentSource ) {\n\t\tif (_shaderProgramCache[fragmentSource]) {\n\t\t\t_currentProgram = _shaderProgramCache[fragmentSource];\n\t\t\tgl.useProgram(_currentProgram.id);\n\t\t\treturn _currentProgram;\n\t\t}\n\n\t\t// Compile shaders\n\t\t_currentProgram = new WebGLProgram( gl, SHADER.VERTEX_IDENTITY, fragmentSource );\n\n\t\tvar floatSize = Float32Array.BYTES_PER_ELEMENT;\n\t\tvar vertSize = 4 * floatSize;\n\t\tgl.enableVertexAttribArray(_currentProgram.attribute.pos);\n\t\tgl.vertexAttribPointer(_currentProgram.attribute.pos, 2, gl.FLOAT, false, vertSize , 0 * floatSize);\n\t\tgl.enableVertexAttribArray(_currentProgram.attribute.uv);\n\t\tgl.vertexAttribPointer(_currentProgram.attribute.uv, 2, gl.FLOAT, false, vertSize, 2 * floatSize);\n\n\t\t_shaderProgramCache[fragmentSource] = _currentProgram;\n\t\treturn _currentProgram;\n\t};\n\n\n\tvar DRAW = { INTERMEDIATE: 1 };\n\n\tvar SHADER = {};\n\tSHADER.VERTEX_IDENTITY = [\n\t\t'precision highp float;',\n\t\t'attribute vec2 pos;',\n\t\t'attribute vec2 uv;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform float flipY;',\n\n\t\t'void main(void) {',\n\t\t\t'vUv = uv;',\n\t\t\t'gl_Position = vec4(pos.x, pos.y*flipY, 0.0, 1.);',\n\t\t'}'\n\t].join('\\n');\n\n\tSHADER.FRAGMENT_IDENTITY = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform sampler2D texture;',\n\n\t\t'void main(void) {',\n\t\t\t'gl_FragColor = texture2D(texture, vUv);',\n\t\t'}',\n\t].join('\\n');\n\n\n\tvar _filter = {};\n\n\n\n\t// -------------------------------------------------------------------------\n\t// Color Matrix Filter\n\n\t_filter.colorMatrix = function( matrix , amount = 1 ) {\n\t\tmatrix = matrix.map((coef, index) => weightedAvg(coef, identityMatrix[index], amount));\n\t\t// Create a Float32 Array and normalize the offset component to 0-1\n\t\tvar m = new Float32Array(matrix);\n\t\tm[4] /= 255;\n\t\tm[9] /= 255;\n\t\tm[14] /= 255;\n\t\tm[19] /= 255;\n\n\t\t// Can we ignore the alpha value? Makes things a bit faster.\n\t\tvar shader = (1==m[18]&&0==m[3]&&0==m[8]&&0==m[13]&&0==m[15]&&0==m[16]&&0==m[17]&&0==m[19])\n\t\t\t? _filter.colorMatrix.SHADER.WITHOUT_ALPHA\n\t\t\t: _filter.colorMatrix.SHADER.WITH_ALPHA;\n\t\t\n\t\tvar program = _compileShader(shader);\n\t\tgl.uniform1fv(program.uniform.m, m);\n\t\t_draw();\n\t};\n\n\t_filter.colorMatrix.SHADER = {};\n\t_filter.colorMatrix.SHADER.WITH_ALPHA = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform sampler2D texture;',\n\t\t'uniform float m[20];',\n\n\t\t'void main(void) {',\n\t\t\t'vec4 c = texture2D(texture, vUv);',\n\t\t\t'gl_FragColor.r = m[0] * c.r + m[1] * c.g + m[2] * c.b + m[3] * c.a + m[4];',\n\t\t\t'gl_FragColor.g = m[5] * c.r + m[6] * c.g + m[7] * c.b + m[8] * c.a + m[9];',\n\t\t\t'gl_FragColor.b = m[10] * c.r + m[11] * c.g + m[12] * c.b + m[13] * c.a + m[14];',\n\t\t\t'gl_FragColor.a = m[15] * c.r + m[16] * c.g + m[17] * c.b + m[18] * c.a + m[19];',\n\t\t'}',\n\t].join('\\n');\n\t_filter.colorMatrix.SHADER.WITHOUT_ALPHA = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform sampler2D texture;',\n\t\t'uniform float m[20];',\n\n\t\t'void main(void) {',\n\t\t\t'vec4 c = texture2D(texture, vUv);',\n\t\t\t'gl_FragColor.r = m[0] * c.r + m[1] * c.g + m[2] * c.b + m[4];',\n\t\t\t'gl_FragColor.g = m[5] * c.r + m[6] * c.g + m[7] * c.b + m[9];',\n\t\t\t'gl_FragColor.b = m[10] * c.r + m[11] * c.g + m[12] * c.b + m[14];',\n\t\t\t'gl_FragColor.a = c.a;',\n\t\t'}',\n\t].join('\\n');\n\n\t_filter.brightness = function( brightness ) {\n\t\tvar b = (brightness || 0) + 1;\n\t\t_filter.colorMatrix([\n\t\t\t\tb, 0, 0, 0, 0,\n\t\t\t\t0, b, 0, 0, 0,\n\t\t\t\t0, 0, b, 0, 0,\n\t\t\t\t0, 0, 0, 1, 0\n\t\t]);\n\t};\n\n\t_filter.saturation = function( amount ) {\n\t\tvar x = (amount || 0) * 2/3 + 1;\n\t\tvar y = ((x-1) *-0.5);\n\t\t_filter.colorMatrix([\n\t\t\tx, y, y, 0, 0,\n\t\t\ty, x, y, 0, 0,\n\t\t\ty, y, x, 0, 0,\n\t\t\t0, 0, 0, 1, 0\n\t\t]);\n\t};\n\n\t_filter.desaturate = function() {\n\t\t_filter.saturation(-1);\n\t};\n\n\t_filter.contrast = function( amount ) {\n\t\tvar v = (amount || 0) + 1;\n\t\tvar o = -128 * (v-1);\n\t\t\n\t\t_filter.colorMatrix([\n\t\t\tv, 0, 0, 0, o,\n\t\t\t0, v, 0, 0, o,\n\t\t\t0, 0, v, 0, o,\n\t\t\t0, 0, 0, 1, 0\n\t\t]);\n\t};\n\n\t_filter.negative = function() {\n\t\t_filter.contrast(-2);\n\t};\n\n\t_filter.hue = function( rotation ) {\n\t\trotation = (rotation || 0)/180 * Math.PI;\n\t\tvar cos = Math.cos(rotation),\n\t\t\tsin = Math.sin(rotation),\n\t\t\tlumR = 0.213,\n\t\t\tlumG = 0.715,\n\t\t\tlumB = 0.072;\n\n\t\t_filter.colorMatrix([\n\t\t\tlumR+cos*(1-lumR)+sin*(-lumR),lumG+cos*(-lumG)+sin*(-lumG),lumB+cos*(-lumB)+sin*(1-lumB),0,0,\n\t\t\tlumR+cos*(-lumR)+sin*(0.143),lumG+cos*(1-lumG)+sin*(0.140),lumB+cos*(-lumB)+sin*(-0.283),0,0,\n\t\t\tlumR+cos*(-lumR)+sin*(-(1-lumR)),lumG+cos*(-lumG)+sin*(lumG),lumB+cos*(1-lumB)+sin*(lumB),0,0,\n\t\t\t0, 0, 0, 1, 0\n\t\t]);\n\t};\n\n\t_filter.desaturateLuminance = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t0.2764723, 0.9297080, 0.0938197, 0, -37.1,\n\t\t\t0.2764723, 0.9297080, 0.0938197, 0, -37.1,\n\t\t\t0.2764723, 0.9297080, 0.0938197, 0, -37.1,\n\t\t\t0, 0, 0, 1, 0\n\t\t], amount);\n\t};\n\n\t_filter.sepia = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t0.393, 0.7689999, 0.18899999, 0, 0,\n\t\t\t0.349, 0.6859999, 0.16799999, 0, 0,\n\t\t\t0.272, 0.5339999, 0.13099999, 0, 0,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.brownie = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t0.5997023498159715,0.34553243048391263,-0.2708298674538042,0,47.43192855600873,\n\t\t\t-0.037703249837783157,0.8609577587992641,0.15059552388459913,0,-36.96841498319127,\n\t\t\t0.24113635128153335,-0.07441037908422492,0.44972182064877153,0,-7.562075277591283,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.vintagePinhole = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t0.6279345635605994,0.3202183420819367,-0.03965408211312453,0,9.651285835294123,\n\t\t\t0.02578397704808868,0.6441188644374771,0.03259127616149294,0,7.462829176470591,\n\t\t\t0.0466055556782719,-0.0851232987247891,0.5241648018700465,0,5.159190588235296,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.kodachrome = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t1.1285582396593525,-0.3967382283601348,-0.03992559172921793,0,63.72958762196502,\n\t\t\t-0.16404339962244616,1.0835251566291304,-0.05498805115633132,0,24.732407896706203,\n\t\t\t-0.16786010706155763,-0.5603416277695248,1.6014850761964943,0,35.62982807460946,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.technicolor = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t1.9125277891456083,-0.8545344976951645,-0.09155508482755585,0,11.793603434377337,\n\t\t\t-0.3087833385928097,1.7658908555458428,-0.10601743074722245,0,-70.35205161461398,\n\t\t\t-0.231103377548616,-0.7501899197440212,1.847597816108189,0,30.950940869491138,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.polaroid = function( amount ) {\n\t\t_filter.colorMatrix([\n\t\t\t1.438,-0.062,-0.062,0,0,\n\t\t\t-0.122,1.378,-0.122,0,0,\n\t\t\t-0.016,-0.016,1.483,0,0,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\t_filter.shiftToBGR = function(amount) {\n\t\t_filter.colorMatrix([\n\t\t\t0,0,1,0,0,\n\t\t\t0,1,0,0,0,\n\t\t\t1,0,0,0,0,\n\t\t\t0,0,0,1,0\n\t\t], amount);\n\t};\n\n\n\t// -------------------------------------------------------------------------\n\t// Convolution Filter\n\n\t_filter.convolution = function( matrix ) {\n\t\tvar m = new Float32Array(matrix);\n\t\tvar pixelSizeX = 1 / _width;\n\t\tvar pixelSizeY = 1 / _height;\n\n\t\tvar program = _compileShader(_filter.convolution.SHADER);\n\t\tgl.uniform1fv(program.uniform.m, m);\n\t\tgl.uniform2f(program.uniform.px, pixelSizeX, pixelSizeY);\n\t\t_draw();\n\t};\n\n\t_filter.convolution.SHADER = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform sampler2D texture;',\n\t\t'uniform vec2 px;',\n\t\t'uniform float m[9];',\n\n\t\t'void main(void) {',\n\t\t\t'vec4 c11 = texture2D(texture, vUv - px);', // top left\n\t\t\t'vec4 c12 = texture2D(texture, vec2(vUv.x, vUv.y - px.y));', // top center\n\t\t\t'vec4 c13 = texture2D(texture, vec2(vUv.x + px.x, vUv.y - px.y));', // top right\n\n\t\t\t'vec4 c21 = texture2D(texture, vec2(vUv.x - px.x, vUv.y) );', // mid left\n\t\t\t'vec4 c22 = texture2D(texture, vUv);', // mid center\n\t\t\t'vec4 c23 = texture2D(texture, vec2(vUv.x + px.x, vUv.y) );', // mid right\n\n\t\t\t'vec4 c31 = texture2D(texture, vec2(vUv.x - px.x, vUv.y + px.y) );', // bottom left\n\t\t\t'vec4 c32 = texture2D(texture, vec2(vUv.x, vUv.y + px.y) );', // bottom center\n\t\t\t'vec4 c33 = texture2D(texture, vUv + px );', // bottom right\n\n\t\t\t'gl_FragColor = ',\n\t\t\t\t'c11 * m[0] + c12 * m[1] + c22 * m[2] +',\n\t\t\t\t'c21 * m[3] + c22 * m[4] + c23 * m[5] +',\n\t\t\t\t'c31 * m[6] + c32 * m[7] + c33 * m[8];',\n\t\t\t'gl_FragColor.a = c22.a;',\n\t\t'}',\n\t].join('\\n');\n\n\n\t_filter.detectEdges = function() {\n\t\t_filter.convolution.call(this, [\n\t\t\t0, 1, 0,\n\t\t\t1, -4, 1,\n\t\t\t0, 1, 0\n\t\t]);\n\t};\n\n\t_filter.sobelX = function() {\n\t\t_filter.convolution.call(this, [\n\t\t\t-1, 0, 1,\n\t\t\t-2, 0, 2,\n\t\t\t-1, 0, 1\n\t\t]);\n\t};\n\n\t_filter.sobelY = function() {\n\t\t_filter.convolution.call(this, [\n\t\t\t-1, -2, -1,\n\t\t\t 0,  0,  0,\n\t\t\t 1,  2,  1\n\t\t]);\n\t};\n\n\t_filter.sharpen = function( amount ) {\n\t\tvar a = amount || 1;\n\t\t_filter.convolution.call(this, [\n\t\t\t0, -1*a, 0,\n\t\t\t-1*a, 1 + 4*a, -1*a,\n\t\t\t0, -1*a, 0\n\t\t]);\n\t};\n\n\t_filter.emboss = function( size ) {\n\t\tvar s = size || 1;\n\t\t_filter.convolution.call(this, [\n\t\t\t-2*s, -1*s, 0,\n\t\t\t-1*s, 1, 1*s,\n\t\t\t0, 1*s, 2*s\n\t\t]);\n\t};\n\n\n\t// -------------------------------------------------------------------------\n\t// Blur Filter\n\n\t_filter.blur = function( size ) {\n\t\tvar blurSizeX = (size/7) / _width;\n\t\tvar blurSizeY = (size/7) / _height;\n\n\t\tvar program = _compileShader(_filter.blur.SHADER);\n\n\t\t// Vertical\n\t\tgl.uniform2f(program.uniform.px, 0, blurSizeY);\n\t\t_draw(DRAW.INTERMEDIATE);\n\n\t\t// Horizontal\n\t\tgl.uniform2f(program.uniform.px, blurSizeX, 0);\n\t\t_draw();\n\t};\n\n\t_filter.blur.SHADER = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform sampler2D texture;',\n\t\t'uniform vec2 px;',\n\n\t\t'void main(void) {',\n\t\t\t'gl_FragColor = vec4(0.0);',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-7.0*px.x, -7.0*px.y))*0.0044299121055113265;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-6.0*px.x, -6.0*px.y))*0.00895781211794;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-5.0*px.x, -5.0*px.y))*0.0215963866053;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-4.0*px.x, -4.0*px.y))*0.0443683338718;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-3.0*px.x, -3.0*px.y))*0.0776744219933;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-2.0*px.x, -2.0*px.y))*0.115876621105;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2(-1.0*px.x, -1.0*px.y))*0.147308056121;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv                             )*0.159576912161;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 1.0*px.x,  1.0*px.y))*0.147308056121;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 2.0*px.x,  2.0*px.y))*0.115876621105;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 3.0*px.x,  3.0*px.y))*0.0776744219933;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 4.0*px.x,  4.0*px.y))*0.0443683338718;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 5.0*px.x,  5.0*px.y))*0.0215963866053;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 6.0*px.x,  6.0*px.y))*0.00895781211794;',\n\t\t\t'gl_FragColor += texture2D(texture, vUv + vec2( 7.0*px.x,  7.0*px.y))*0.0044299121055113265;',\n\t\t'}',\n\t].join('\\n');\n\n\n\t// -------------------------------------------------------------------------\n\t// Pixelate Filter\n\n\t_filter.pixelate = function( size ) {\n\t\tvar blurSizeX = (size) / _width;\n\t\tvar blurSizeY = (size) / _height;\n\n\t\tvar program = _compileShader(_filter.pixelate.SHADER);\n\n\t\t// Horizontal\n\t\tgl.uniform2f(program.uniform.size, blurSizeX, blurSizeY);\n\t\t_draw();\n\t};\n\n\t_filter.pixelate.SHADER = [\n\t\t'precision highp float;',\n\t\t'varying vec2 vUv;',\n\t\t'uniform vec2 size;',\n\t\t'uniform sampler2D texture;',\n\n\t\t'vec2 pixelate(vec2 coord, vec2 size) {',\n\t\t\t'return floor( coord / size ) * size;',\n\t\t'}',\n\n\t\t'void main(void) {',\n\t\t\t'gl_FragColor = vec4(0.0);',\n\t\t\t'vec2 coord = pixelate(vUv, size);',\n\t\t\t'gl_FragColor += texture2D(texture, coord);',\n\t\t'}',\n\t].join('\\n');\n};\n\n})(window);\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", "/** @odoo-module **/\n\nimport './commands/deleteBackward.js';\nimport './commands/deleteForward.js';\nimport './commands/enter.js';\nimport './commands/shiftEnter.js';\nimport './commands/shiftTab.js';\nimport './commands/tab.js';\nimport './commands/toggleList.js';\nimport './commands/align.js';\n\nimport { sanitize } from './utils/sanitize.js';\nimport { serializeNode, unserializeNode, serializeSelection } from './utils/serialize.js';\nimport {\n    closestBlock,\n    commonParentGet,\n    containsUnremovable,\n    DIRECTIONS,\n    ensureFocus,\n    getCursorDirection,\n    getFurthestUneditableParent,\n    getListMode,\n    getOuid,\n    insertText,\n    isColorGradient,\n    nodeSize,\n    preserveCursor,\n    setCursorStart,\n    setSelection,\n    toggleClass,\n    closestElement,\n    isVisible,\n    isHtmlContentSupported,\n    rgbToHex,\n    isIconElement,\n    ICON_SELECTOR,\n    getInSelection,\n    getDeepRange,\n    getRowIndex,\n    getColumnIndex,\n    ancestors,\n    firstLeaf,\n    previousLeaf,\n    nextLeaf,\n    isUnremovable,\n    fillEmpty,\n    isEmptyBlock,\n    URL_REGEX,\n    isSelectionFormat,\n    YOUTUBE_URL_GET_VIDEO_ID,\n    unwrapContents,\n    peek,\n    getAdjacentPreviousSiblings,\n    getAdjacentNextSiblings,\n    isBlock,\n    getTraversedNodes,\n    getSelectedNodes,\n    descendants,\n    hasValidSelection,\n    hasTableSelection,\n    pxToFloat,\n    parseHTML,\n    splitTextNode,\n    isEditorTab,\n    isMacOS,\n    isProtected,\n    isArtificialVoidElement,\n    cleanZWS,\n    isZWS,\n    setCursorEnd,\n    paragraphRelatedElements,\n    getDeepestPosition,\n    leftPos,\n    isNotAllowedContent,\n    EMAIL_REGEX,\n    prepareUpdate,\n    boundariesOut,\n    getFontSizeDisplayValue,\n    rightLeafOnlyNotBlockPath,\n    lastLeaf,\n    isUnbreakable,\n    splitAroundUntil,\n    ZERO_WIDTH_CHARS,\n    ZERO_WIDTH_CHARS_REGEX,\n    getAdjacentCharacter,\n    isLinkEligibleForZwnbsp,\n} from './utils/utils.js';\nimport { editorCommands } from './commands/commands.js';\nimport { Powerbox } from './powerbox/Powerbox.js';\nimport { TablePicker } from './tablepicker/TablePicker.js';\n\nexport * from './utils/utils.js';\nimport { UNBREAKABLE_ROLLBACK_CODE, UNREMOVABLE_ROLLBACK_CODE } from './utils/constants.js';\n/* global DOMPurify */\n\nconst BACKSPACE_ONLY_COMMANDS = ['oDeleteBackward', 'oDeleteForward'];\nconst BACKSPACE_FIRST_COMMANDS = BACKSPACE_ONLY_COMMANDS.concat(['oEnter', 'oShiftEnter']);\n\n// 60 seconds\nconst HISTORY_SNAPSHOT_INTERVAL = 1000 * 60;\n// 10 seconds\nconst HISTORY_SNAPSHOT_BUFFER_TIME = 1000 * 10;\n\nconst KEYBOARD_TYPES = { VIRTUAL: 'VIRTUAL', PHYSICAL: 'PHYSICAL', UNKNOWN: 'UKNOWN' };\n\nexport const AVATAR_SIZE = 25;\n\nconst IS_KEYBOARD_EVENT_UNDO = ev => ev.key === 'z' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_REDO = ev => ev.key === 'y' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_BOLD = ev => ev.key === 'b' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_ITALIC = ev => ev.key === 'i' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_UNDERLINE = ev => ev.key === 'u' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_STRIKETHROUGH = ev => ev.key === '5' && (ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_LEFT_ARROW = ev => ev.key === 'ArrowLeft' && !(ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_RIGHT_ARROW = ev => ev.key === 'ArrowRight' && !(ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_UP_ARROW = ev => ev.key === 'ArrowUp' && !(ev.ctrlKey || ev.metaKey);\nconst IS_KEYBOARD_EVENT_DOWN_ARROW = ev => ev.key === 'ArrowDown' && !(ev.ctrlKey || ev.metaKey);\n\nconst CLIPBOARD_BLACKLISTS = {\n    unwrap: ['.Apple-interchange-newline', 'DIV'], // These elements' children will be unwrapped.\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\n// Commands that don't require a DOM selection but take an argument instead.\nconst SELECTIONLESS_COMMANDS = ['addRow', 'addColumn', 'removeRow', 'removeColumn', 'resetSize'];\n\nconst FORMATTING_COMMANDS = ['applyColor', 'bold', 'italic', 'underline', 'strikeThrough', 'setFontSize']\n\nfunction defaultOptions(defaultObject, object) {\n    const newObject = Object.assign({}, defaultObject, object);\n    for (const [key, value] of Object.entries(object)) {\n        if (typeof value === 'undefined') {\n            newObject[key] = defaultObject[key];\n        }\n    }\n    return newObject;\n}\nfunction getImageFiles(dataTransfer) {\n    return [...dataTransfer.items]\n        .filter(item => item.kind === 'file' && item.type.includes('image/'))\n        .map((item) => item.getAsFile());\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}\nexport class OdooEditor extends EventTarget {\n    constructor(editable, options = {}) {\n        super();\n\n        this.options = defaultOptions(\n            {\n                controlHistoryFromDocument: false,\n                getContextFromParentRect: () => {\n                    return { top: 0, left: 0 };\n                },\n                getScrollContainerRect: () => document.body.getBoundingClientRect(),\n                toSanitize: true,\n                isRootEditable: true,\n                placeholder: false,\n                showEmptyElementHint: true,\n                defaultLinkAttributes: {},\n                plugins: [],\n                getUnremovableElements: () => [],\n                getReadOnlyAreas: () => [],\n                getContentEditableAreas: () => [],\n                getPowerboxElement: () => {\n                    const selection = document.getSelection();\n                    if (selection.isCollapsed && selection.rangeCount) {\n                        const elementSelectors = ['DIV', 'LI', ...paragraphRelatedElements];\n                        return closestElement(selection.anchorNode, elementSelectors.join(', '));\n                    }\n                },\n                preHistoryUndo: () => {},\n                beforeAnyCommand: () => {},\n                isHintBlacklisted: () => false,\n                filterMutationRecords: (records) => records,\n                /**\n                 * In case an external asynchronous post processing has to be\n                 * applied on some nodes after an external step (i.e. render\n                 * an OWL Component), the owner of the post-processing will\n                 * return a Promise through this hook resolved when it is done.\n                 * Further collaborative external steps will be buffered as\n                 * long as that promise is not resolved, to avoid a situation\n                 * where the editor tries to apply mutations inside a node that\n                 * is currently being rendered (not ready).\n                 *\n                 * @param {Element} editable\n                 * @returns {Promise|null} Promise that will be resolved when\n                 *          the rendering is done, or null if there is no\n                 *          rendering to do. The editor will buffer new external\n                 *          steps (collaborative) until the promise is resolved.\n                 */\n                postProcessExternalSteps: () => null,\n                onPostSanitize: () => {},\n                direction: 'ltr',\n                _t: string => string,\n                allowCommandVideo: true,\n                renderingClasses: [],\n                allowInlineAtRoot: false,\n                useResponsiveFontSizes: true,\n                showResponsiveFontSizesBadges: false,\n                showExtendedTextStylesOptions: false,\n                autoActivateContentEditable: true,\n                // TODO probably move `getCSSVariableValue` and\n                // `convertNumericToUnit` as odoo-editor utils to avoid this\n                getCSSVariableValue: () => null,\n                convertNumericToUnit: x => x,\n            },\n            options,\n        );\n\n        // --------------\n        // Set properties\n        // --------------\n\n        this.document = options.document || document;\n        this.isDestroyed = false;\n\n        this.isMobile = matchMedia('(max-width: 767px)').matches;\n        this.isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1;\n\n        this.isPrepareUpdateLocked = false;\n\n        // Keyboard type detection, happens only at the first keydown event.\n        this.keyboardType = KEYBOARD_TYPES.UNKNOWN;\n\n        // Wether we should check for unbreakable the next history step.\n        this._checkStepUnbreakable = true;\n\n        // All dom listeners currently active.\n        this._domListeners = [];\n\n        // Set of labels that which prevent the automatic step mechanism if\n        // it contains at least one element.\n        this._observerTimeoutUnactive = new Set();\n        // Set of labels that which prevent the observer to be active if\n        // it contains at least one element.\n        this._observerUnactiveLabels = new Set();\n\n        // The state of the dom.\n        this._currentMouseState = 'mouseup';\n\n        this._onKeyupResetContenteditableNodes = [];\n\n        // Track if we need to rollback mutations in case unbreakable or unremovable are being added or removed.\n        this._toRollback = false;\n\n        // Map that from an node id to the dom node.\n        this._idToNodeMap = new Map();\n\n        // Instanciate plugins.\n        this._plugins = [];\n        for (const plugin of this.options.plugins) {\n            this._pluginAdd(plugin);\n        }\n\n        // -------------------\n        // Alter the editable\n        // -------------------\n\n        if (editable.innerHTML.trim() === '') {\n            editable.innerHTML = '<p><br></p>';\n        }\n        this.initElementForEdition(editable);\n\n        // Convention: root node is ID root.\n        editable.oid = 'root';\n        this._idToNodeMap.set(1, editable);\n        this.editable = editable;\n        this.editable.classList.add(\"odoo-editor-editable\");\n        if (this.options.toSanitize) {\n            sanitize(editable);\n            this.options.onPostSanitize(editable);\n        }\n        this.editable.setAttribute('dir', this.options.direction);\n\n        // Set contenteditable before clone as FF updates the content at this point.\n        this.canActivateContentEditable = this.options.autoActivateContentEditable;\n        if (this.canActivateContentEditable) {\n            this._activateContenteditable();\n        }\n        this._collabClientId = this.options.collaborationClientId;\n        this._collabClientAvatarUrl = this.options.collaborationClientAvatarUrl;\n\n        // Collaborator selection and caret display.\n        this._collabSelectionInfos = new Map();\n        this._collabSelectionColor = `hsl(${(Math.random() * 360).toFixed(0)}, 75%, 50%)`;\n        this._avatarsOverlaps = {}\n\n        // This main container is used to contain a tree of sub containers.\n        // By having one parent that contains a tree of containers, it is easy\n        // to change the z-index of any container by changing their place in the\n        // tree rather than tweaking a z-index number.\n        this.mainAbsoluteContainer = this.document.createElement('div');\n        this.mainAbsoluteContainer.classList.add('oe-absolute-container');\n        this.editable.before(this.mainAbsoluteContainer);\n\n        // This container contains the users selections.\n        this._selectionsContainer = this.makeAbsoluteContainer('oe-selections-container');\n        // This container contains the users avatars.\n        this._avatarsContainer = this.makeAbsoluteContainer('oe-avatars-container');\n        // This container contains the users counter that overlap the users avatars.\n        this._avatarsCountersContainer = this.makeAbsoluteContainer('oe-avatars-counters-container');\n\n        // Promise for extra rendering, collaborative external steps will be\n        // buffered (delayed) until it is resolved.\n        this._postProcessExternalStepsPromise = null;\n        this._externalStepsBuffer = [];\n\n        this.idSet(editable);\n        this._historyStepsActive = true;\n        this.historyReset();\n        if (this.options.initialHistoryId) {\n            this.historySetInitialId(this.options.initialHistoryId);\n        }\n\n        this._pluginCall('start', [editable]);\n        this._pluginCall('sanitizeElement', [editable]);\n\n        // ------\n        // Tables\n        // ------\n\n        // Create the table picker for the Powerbox.\n        this.powerboxTablePicker = new TablePicker({\n            document: this.document,\n            floating: true,\n            getContextFromParentRect: this.options.getContextFromParentRect,\n            direction: this.options.direction,\n        });\n        document.body.appendChild(this.powerboxTablePicker.el);\n        this.powerboxTablePicker.addEventListener('cell-selected', ev => {\n            this.execCommand('insertTable', {\n                rowNumber: ev.detail.rowNumber,\n                colNumber: ev.detail.colNumber,\n            });\n        });\n        // Create the table UI.\n        this._tableUiContainer = this.document.createElement('div');\n        this._tableUiContainer.classList.add('o_table_ui_container');\n        const parser = new DOMParser();\n        const isRTL = this.options.direction === \"rtl\";\n        for (const direction of ['row', 'column']) {\n            // Create the containers and the menu toggler.\n            const iconClass = (direction === 'row') ? 'fa-ellipsis-v' : 'fa-ellipsis-h';\n            const ui = parser.parseFromString(`<div class=\"o_table_ui o_${direction}_ui\" style=\"visibility: hidden;\">\n                <div>\n                    <span class=\"o_table_ui_menu_toggler fa ${iconClass}\"></span>\n                    <div class=\"o_table_ui_menu\"></div>\n                </div>\n            </div>`, 'text/html').body.firstElementChild;\n            const uiMenu = ui.querySelector('.o_table_ui_menu');\n            // Create the move buttons.\n            if (direction === 'column') {\n                if (isRTL) {\n                    uiMenu.append(...parser.parseFromString(`\n                        <div class=\"o_move_right\"><span class=\"fa fa-chevron-right\"></span>` + this.options._t('Move left') + `</div>\n                        <div class=\"o_move_left\"><span class=\"fa fa-chevron-left\"></span>` + this.options._t('Move right') + `</div>\n                    `, 'text/html').body.children);\n                } else {\n                    uiMenu.append(...parser.parseFromString(`\n                        <div class=\"o_move_left\"><span class=\"fa fa-chevron-left\"></span>` + this.options._t('Move left') + `</div>\n                        <div class=\"o_move_right\"><span class=\"fa fa-chevron-right\"></span>` + this.options._t('Move right') + `</div>\n                    `, 'text/html').body.children);\n                }\n                this.addDomListener(uiMenu.querySelector('.o_move_left'), 'click', this._onTableMoveLeftClick);\n                this.addDomListener(uiMenu.querySelector('.o_move_right'), 'click', this._onTableMoveRightClick);\n            } else {\n                uiMenu.append(...parser.parseFromString(`\n                    <div class=\"o_move_up\"><span class=\"fa fa-chevron-left\" style=\"transform: rotate(90deg);\"></span>` + this.options._t('Move up') + `</div>\n                    <div class=\"o_move_down\"><span class=\"fa fa-chevron-right\" style=\"transform: rotate(90deg);\"></span>` + this.options._t('Move down') + `</div>\n                `, 'text/html').body.children);\n                this.addDomListener(uiMenu.querySelector('.o_move_up'), 'click', this._onTableMoveUpClick);\n                this.addDomListener(uiMenu.querySelector('.o_move_down'), 'click', this._onTableMoveDownClick);\n            }\n\n            // Create the add buttons.\n            if (direction === 'column') {\n                if (isRTL) {\n                    uiMenu.append(...parser.parseFromString(`\n                        <div class=\"o_insert_right\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert left') + `</div>\n                        <div class=\"o_insert_left\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert right') + `</div>\n                    `, 'text/html').body.children);\n                } else {\n                    uiMenu.append(...parser.parseFromString(`\n                        <div class=\"o_insert_left\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert left') + `</div>\n                        <div class=\"o_insert_right\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert right') + `</div>\n                    `, 'text/html').body.children);\n                }\n                this.addDomListener(uiMenu.querySelector('.o_insert_left'), 'click', () => this.execCommand('addColumn', 'before', this._columnUiTarget));\n                this.addDomListener(uiMenu.querySelector('.o_insert_right'), 'click', () => this.execCommand('addColumn', 'after', this._columnUiTarget));\n            } else {\n                uiMenu.append(...parser.parseFromString(`\n                    <div class=\"o_insert_above\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert above') + `</div>\n                    <div class=\"o_insert_below\"><span class=\"fa fa-plus\"></span>` + this.options._t('Insert below') + `</div>\n                `, 'text/html').body.children);\n                this.addDomListener(uiMenu.querySelector('.o_insert_above'), 'click', () => this.execCommand('addRow', 'before', this._rowUiTarget));\n                this.addDomListener(uiMenu.querySelector('.o_insert_below'), 'click', () => this.execCommand('addRow', 'after', this._rowUiTarget));\n            }\n\n            // Add the delete button.\n            if (direction === 'column') {\n                uiMenu.append(parser.parseFromString(`<div class=\"o_delete_column\"><span class=\"fa fa-trash\"></span>` + this.options._t('Delete') + `</div>\n                `, 'text/html').body.firstChild)\n                this.addDomListener(uiMenu.querySelector('.o_delete_column'), 'click', this._onTableDeleteColumnClick);\n            } else {\n                uiMenu.append(parser.parseFromString(`<div class=\"o_delete_row\"><span class=\"fa fa-trash\"></span>` + this.options._t('Delete') + `</div>\n                `, 'text/html').body.firstChild)\n                this.addDomListener(uiMenu.querySelector('.o_delete_row'), 'click', this._onTableDeleteRowClick);\n            }\n\n            // Reset the size of the table\n            uiMenu.append(parser.parseFromString(`<div class=\"o_reset_table_size\"><span class=\"fa fa-table\"></span>` + this.options._t('Reset Size') + `</div>\n                `, 'text/html').body.firstChild)\n            this.addDomListener(uiMenu.querySelector('.o_reset_table_size'), 'click', () => this.execCommand('resetSize', this._tableUiTarget));\n\n            this[`_${direction}Ui`] = ui;\n            this._tableUiContainer.append(ui);\n            this.addDomListener(ui.querySelector('.o_table_ui_menu_toggler'), 'click', this._onTableMenuTogglerClick);\n            this.editable.before(this._tableUiContainer);\n        }\n\n        // --------\n        // Powerbox\n        // --------\n\n        this.powerbox = new Powerbox({\n            editable: this.editable,\n            getContextFromParentRect: this.options.getContextFromParentRect,\n            commandFilters: this.options.powerboxFilters,\n            onShow: () => {\n                this.powerboxTablePicker.hide();\n            },\n            beforeCommand: () => {\n                if (this._isPowerboxOpenOnInput) {\n                    this.historyRevertUntil(this._powerboxBeforeStepIndex);\n                    this.historyStep(true);\n                    this._historyStepsStates.set(peek(this._historySteps).id, 'consumed');\n                    ensureFocus(this.editable);\n                    getDeepRange(this.editable, { select: true });\n                }\n            },\n            afterCommand: () => {\n                this.historyStep(true);\n                this._isPowerboxOpenOnInput = false;\n            },\n            categories: [\n                { name: this.options._t('Structure'), priority: 70 },\n                { name: this.options._t('Format'), priority: 60 },\n                { name: this.options._t('Widgets'), priority: 30 },\n                ...(this.options.categories || []),\n            ],\n            commands: [\n                {\n                    category: this.options._t('Structure'),\n                    name: this.options._t('Bulleted list'),\n                    priority: 110,\n                    description: this.options._t('Create a simple bulleted list'),\n                    fontawesome: 'fa-list-ul',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('toggleList', 'UL');\n                    },\n                },\n                {\n                    category: this.options._t('Structure'),\n                    name: this.options._t('Numbered list'),\n                    priority: 100,\n                    description: this.options._t('Create a list with numbering'),\n                    fontawesome: 'fa-list-ol',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('toggleList', 'OL');\n                    },\n                },\n                {\n                    category: this.options._t('Structure'),\n                    name: this.options._t('Checklist'),\n                    priority: 90,\n                    description: this.options._t('Track tasks with a checklist'),\n                    fontawesome: 'fa-check-square-o',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('toggleList', 'CL');\n                    },\n                },\n                {\n                    category: this.options._t('Structure'),\n                    name: this.options._t('Table'),\n                    priority: 80,\n                    description: this.options._t('Insert a table'),\n                    fontawesome: 'fa-table',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        if(this.isMobile){\n                            this.execCommand('insertTable', {\n                                rowNumber: this.powerboxTablePicker.rowNumber,\n                                colNumber: this.powerboxTablePicker.colNumber,\n                            });\n                        } else {\n                            this.powerboxTablePicker.show();\n                        }\n                    },\n                },\n                {\n                    category: this.options._t('Format'),\n                    name: this.options._t('Heading 1'),\n                    priority: 50,\n                    description: this.options._t('Big section heading'),\n                    fontawesome: 'fa-header',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('setTag', 'H1');\n                    },\n                },\n                {\n                    category: this.options._t('Format'),\n                    name: this.options._t('Heading 2'),\n                    priority: 40,\n                    description: this.options._t('Medium section heading'),\n                    fontawesome: 'fa-header',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('setTag', 'H2');\n                    },\n                },\n                {\n                    category: this.options._t('Format'),\n                    name: this.options._t('Heading 3'),\n                    priority: 30,\n                    description: this.options._t('Small section heading'),\n                    fontawesome: 'fa-header',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('setTag', 'H3');\n                    },\n                },\n                {\n                    category: this.options._t('Format'),\n                    name: this.options._t('Switch direction'),\n                    priority: 20,\n                    description: this.options._t('Switch the text\\'s direction'),\n                    fontawesome: 'fa-exchange',\n                    callback: () => {\n                        this.execCommand('switchDirection');\n                    },\n                },\n                {\n                    category: this.options._t('Format'),\n                    name: this.options._t('Text'),\n                    priority: 10,\n                    description: this.options._t('Paragraph block'),\n                    fontawesome: 'fa-paragraph',\n                    isDisabled: () => !this.isSelectionInBlockRoot(),\n                    callback: () => {\n                        this.execCommand('setTag', 'P');\n                    },\n                },\n                {\n                    category: this.options._t('Widgets'),\n                    name: this.options._t('3 Stars'),\n                    priority: 20,\n                    description: this.options._t('Insert a rating over 3 stars'),\n                    fontawesome: 'fa-star-o',\n                    callback: () => {\n                        let html = '\\u200B<span contenteditable=\"false\" class=\"o_stars o_three_stars\">';\n                        html += Array(3).fill().map(() => '<i class=\"fa fa-star-o\"></i>').join('');\n                        html += '</span>\\u200B';\n                        this.execCommand('insert', parseHTML(this.document, html));\n                    },\n                },\n                {\n                    category: this.options._t('Widgets'),\n                    name: this.options._t('5 Stars'),\n                    priority: 10,\n                    description: this.options._t('Insert a rating over 5 stars'),\n                    fontawesome: 'fa-star',\n                    callback: () => {\n                        let html = '\\u200B<span contenteditable=\"false\" class=\"o_stars o_five_stars\">';\n                        html += Array(5).fill().map(() => '<i class=\"fa fa-star-o\"></i>').join('');\n                        html += '</span>\\u200B';\n                        this.execCommand('insert', parseHTML(this.document, html));\n                    },\n                },\n                ...(this.options.commands || []),\n                ...(!this.options.commands || !this.options.commands.find(c =>  c.name === this.options._t('Separator')) ? [\n                    {\n                        category: this.options._t('Structure'),\n                        name: this.options._t('Separator'),\n                        priority: 40,\n                        description: this.options._t('Insert a horizontal rule separator'),\n                        fontawesome: 'fa-minus',\n                        isDisabled: () => !this.isSelectionInBlockRoot(),\n                        callback: () => {\n                            this.execCommand('insertHorizontalRule');\n                        },\n                    }] : []),\n            ],\n        });\n\n        // -----------\n        // Bind events\n        // -----------\n\n        this.observerActive();\n\n        this.addDomListener(this.editable, 'keydown', this._onKeyDown);\n        this.addDomListener(this.editable, 'input', this._onInput);\n        this.addDomListener(this.editable, 'beforeinput', this._onBeforeInput);\n        this.addDomListener(this.editable, 'mousedown', this._onMouseDown);\n        this.addDomListener(this.editable, 'mouseup', this._onMouseup);\n        this.addDomListener(this.editable, 'mousemove', this._onMousemove);\n        this.addDomListener(this.editable, 'mouseleave', this._onMouseLeave);\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        this.addDomListener(this.editable, 'copy', this._onClipboardCopy);\n        this.addDomListener(this.editable, 'cut', this._onClipboardCut);\n\n        this.addDomListener(this.document, 'selectionchange', this._onSelectionChange);\n        this.addDomListener(this.document, 'selectionchange', this._handleCommandHint);\n        this.addDomListener(this.document, 'keydown', this._onDocumentKeydown);\n        this.addDomListener(this.document, 'keyup', this._onDocumentKeyup);\n        this.addDomListener(this.document, 'mouseup', this._onDocumentMouseup);\n        this.addDomListener(this.document, 'click', this._onDocumentClick);\n        this.addDomListener(this.document, 'scroll', this._onScroll, true);\n\n        this.multiselectionRefresh = this.multiselectionRefresh.bind(this);\n        this._resizeObserver = new ResizeObserver(this.multiselectionRefresh);\n        this._resizeObserver.observe(this.document.body);\n        this._resizeObserver.observe(this.editable);\n        this.addDomListener(this.editable, 'scroll', this.multiselectionRefresh);\n\n        if (this._collabClientId) {\n            this._snapshotInterval = setInterval(() => {\n                this._historyMakeSnapshot();\n            }, HISTORY_SNAPSHOT_INTERVAL);\n        }\n\n        // -------\n        // Toolbar\n        // -------\n\n        if (this.options.toolbar) {\n            this.setupToolbar(this.options.toolbar);\n        }\n        // placeholder hint\n        if (editable.textContent === '' && this.options.placeholder) {\n            this._makeHint(editable.firstChild, this.options.placeholder, true);\n        }\n    }\n    /**\n     * Releases anything that was initialized.\n     *\n     * TODO: properly implement this.\n     */\n    destroy() {\n        this.observerUnactive();\n        this._removeDomListener();\n        this.powerbox.destroy();\n        this.powerboxTablePicker.el.remove();\n        this.mainAbsoluteContainer.remove();\n        this._resizeObserver.disconnect();\n        clearInterval(this._snapshotInterval);\n        this._pluginCall('destroy', []);\n        this.isDestroyed = true;\n        // Remove table UI\n        this._rowUi.remove();\n        this._columnUi.remove();\n    }\n\n    setupToolbar(toolbar) {\n        this.toolbar = toolbar;\n        this.autohideToolbar = this.options.autohideToolbar;\n        if (!this.options.showExtendedTextStylesOptions) {\n            this.toolbar.querySelectorAll(\"[data-extended-text-style]\")\n                .forEach(el => el.classList.add(\"d-none\"));\n        }\n        this.bindExecCommand(this.toolbar);\n        // Ensure anchors in the toolbar don't trigger a hash change.\n        const toolbarAnchors = this.toolbar.querySelectorAll('a');\n        toolbarAnchors.forEach(a => a.addEventListener('click', e => e.preventDefault()));\n        for (const colorLabel of this.toolbar.querySelectorAll('label')) {\n            colorLabel.addEventListener('mousedown', ev => {\n                // Hack to prevent loss of focus (done by preventDefault) while still opening\n                // color picker dialog (which is also prevented by preventDefault on chrome,\n                // except when click detail is 2, which happens on a double-click but isn't\n                // triggered by a dblclick event)\n                if (ev.detail < 2) {\n                    ev.preventDefault();\n                    ev.currentTarget.dispatchEvent(new MouseEvent('click', { detail: 2 }));\n                }\n            });\n            colorLabel.addEventListener('input', ev => {\n                this.document.execCommand(ev.target.name, false, ev.target.value);\n                this.updateColorpickerLabels();\n            });\n        }\n        const fontSizeInput = this.toolbar.querySelector('input#fontSizeCurrentValue');\n        this.addDomListener(this.toolbar, 'click', ev => {\n            if (fontSizeInput && !fontSizeInput.readOnly && ev.target.closest('#font-size .dropdown-toggle')) {\n                // If the click opened the font size dropdown, select the input content.\n                fontSizeInput.select();\n            } else if (\n                !this.isSelectionInEditable() &&\n                ev.target.nodeName !== 'INPUT' &&\n                ev.target.id !== 'image-transform'\n            ) {\n                // Otherwise, if we lost the selection in the editable, restore it.\n                this.historyResetLatestComputedSelection(true);\n            }\n        });\n\n        const applyFontSizeREM = pxStrValue => {\n            const pxValue = parseFloat(pxStrValue);\n            const remValue = this.options.convertNumericToUnit(pxValue, \"px\", \"rem\");\n            this.execCommand(\"setFontSize\", `${remValue}rem`);\n        };\n\n        // Handle the font size input.\n        if (fontSizeInput) {\n            const debouncedOnInputChange = (() => {\n                let handle;\n                return () => new Promise(resolve => {\n                    clearTimeout(handle);\n                    handle = setTimeout(() => {\n                        handle = null;\n                        const fontSize = parseInt(fontSizeInput.value);\n                        if (fontSize > 0) {\n                            getDeepRange(this.editable, { correctTripleClick: true, select: true });\n                            if (!this.isSelectionInEditable()) {\n                                this.historyResetLatestComputedSelection(true);\n                            }\n                            applyFontSizeREM(fontSize);\n                            fontSizeInput.blur();\n                        }\n                        resolve();\n                    }, 50);\n                });\n            })();\n            this.addDomListener(fontSizeInput, 'change', debouncedOnInputChange);\n        }\n\n        // Handle the font size dropdown.\n        const fontSizeDropdown = this.toolbar.querySelector('#font-size');\n        if (fontSizeDropdown) {\n            this.computeFontSizeSelectorValues(fontSizeDropdown);\n\n            const applyFontSizeChoice = optionEl => {\n                if (!this.isSelectionInEditable()) {\n                    this.historyResetLatestComputedSelection(true);\n                }\n                if (this.options.useResponsiveFontSizes) {\n                    const fontSizeClassName = optionEl.dataset.applyClass;\n                    this.execCommand(\"setFontSize\", undefined);\n                    this.historyResetLatestComputedSelection(true);\n                    this.execCommand(\"setFontSizeClassName\", fontSizeClassName);\n                } else {\n                    applyFontSizeREM(optionEl.dataset.value);\n                }\n            };\n            fontSizeDropdown.querySelectorAll('.dropdown-item').forEach(item => {\n                this.addDomListener(item, 'mousedown', ev => {\n                    applyFontSizeChoice(ev.currentTarget);\n                });\n                this.addDomListener(item, 'keydown', ev => {\n                    if (ev.key !== 'Enter') {\n                        return;\n                    }\n                    applyFontSizeChoice(ev.currentTarget);\n                });\n            });\n        }\n\n        this._updateToolbar();\n    }\n\n    /**\n     * Sets the px value for every font size dropdown item.\n     */\n    computeFontSizeSelectorValues(fontSizeDropdownEl) {\n        fontSizeDropdownEl = fontSizeDropdownEl || this.toolbar.querySelector(\"#font-size\");\n        // On some screen size, the fontsize dropdown might be hidden.\n        if (!fontSizeDropdownEl) {\n            return;\n        }\n\n        let previousItem = null;\n        let previousValue = -1;\n        const style = this.document.defaultView.getComputedStyle(this.document.body);\n        for (const itemEl of fontSizeDropdownEl.querySelectorAll(\"[data-dynamic-value]\")) {\n            const variableName = itemEl.dataset.dynamicValue;\n            const strValue = this.options.getCSSVariableValue(variableName, style);\n            const remValue = parseFloat(strValue);\n            const pxValue = this.options.convertNumericToUnit(remValue, \"rem\", \"px\");\n            // Change the text node value only to preserve the badge element\n            const roundedValue = Math.round(pxValue);\n            itemEl.dataset.value = roundedValue;\n            itemEl.firstChild.textContent = roundedValue;\n\n            // If same value as the previous one, hide the previous one\n            if (previousItem) {\n                previousItem.parentElement.classList.toggle('d-none', Math.abs(pxValue - previousValue) < 0.001);\n            }\n            previousItem = itemEl;\n            previousValue = pxValue;\n        }\n\n        for (const badgeEl of fontSizeDropdownEl.querySelectorAll(\".o_we_font_size_badge\")) {\n            badgeEl.classList.toggle(\"d-none\", !this.options.showResponsiveFontSizesBadges);\n        }\n    }\n\n    resetContent(value) {\n        value = value || '<p><br></p>';\n        this.editable.innerHTML = value;\n        this.sanitize(this.editable);\n        this.historyStep(true);\n        // The unbreakable protection mechanism detects an anomaly and attempts\n        // to trigger a rollback when the content is reset using `innerHTML`.\n        // Prevent this rollback as it would otherwise revert the new content.\n        this._toRollback = false;\n        // Placeholder hint.\n        if (this.editable.textContent === '' && this.options.placeholder) {\n            this._makeHint(this.editable.firstChild, this.options.placeholder, true);\n        }\n        this.multiselectionRefresh();\n    }\n\n    sanitize(target) {\n        this.observerFlush();\n        let record;\n        if (!target) {\n            // If the target is not given,\n            // find the closest common ancestor to all the nodes referenced\n            // in the mutations from the last step.\n            for (record of this._currentStep.mutations) {\n                const node = this.idFind(record.parentId || record.id) || this.editable;\n                if (!this.editable.contains(node)) {\n                    continue;\n                }\n                target = target\n                    ? commonParentGet(target, node, this.editable)\n                    : node;\n            }\n        }\n        if (!target) {\n            return false;\n        }\n\n        // If the common ancestor is in a nested list, make sure to sanitize\n        // that list's parent <li> instead, so there is enough context to\n        // potentially merge sibling nested lists\n        // (eg, <ol>\n        //          <li class=\"oe-nested\"><ul>...</ul></li>\n        //          <li class=\"oe-nested\"><ul>...</ul></li>\n        //      </ol>: these two lists should be merged together so the common\n        // ancestor should be the <ol> element).\n        const nestedListAncestor = closestElement(target, '.oe-nested');\n        if (nestedListAncestor && nestedListAncestor.parentElement) {\n            target = nestedListAncestor.parentElement;\n        }\n\n        // sanitize and mark current position as sanitized\n        sanitize(target, this.editable);\n        this._resetLinkInSelection();\n        this._pluginCall('sanitizeElement',\n                         [target.parentElement || target]);\n        this.options.onPostSanitize(target);\n    }\n\n    addDomListener(element, eventName, callback, useCapture) {\n        const boundCallback = callback.bind(this);\n        this._domListeners.push([element, eventName, boundCallback]);\n        element.addEventListener(eventName, boundCallback, useCapture);\n    }\n\n    /**\n     * Make an absolute 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    makeAbsoluteContainer(containerId) {\n        const container = this.document.createElement('div');\n        container.className = `oe-absolute-container`;\n        container.setAttribute('data-oe-absolute-container-id', containerId);\n        this.mainAbsoluteContainer.append(container);\n        return container;\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    // Assign IDs to src, and dest if defined\n    idSet(node, testunbreak = false) {\n        if (!node.oid) {\n            node.oid = this._generateId();\n        }\n        // In case the id was created by another collaboration client.\n        this._idToNodeMap.set(node.oid, node);\n        // Rollback if node.ouid changed. This ensures that nodes never change\n        // unbreakable ancestors.\n        node.ouid = node.ouid || getOuid(node, true);\n        if (testunbreak && !(node.nodeType === Node.TEXT_NODE && !node.length)) {\n            const ouid = getOuid(node);\n            if (!this._toRollback && ouid && ouid !== node.ouid) {\n                this._toRollback = UNBREAKABLE_ROLLBACK_CODE;\n            }\n        }\n\n        let childNode = node.firstChild;\n        while (childNode) {\n            this.idSet(childNode, testunbreak);\n            childNode = childNode.nextSibling;\n        }\n    }\n\n    idFind(id) {\n        return this._idToNodeMap.get(id);\n    }\n\n    serializeNode(node, mutatedNodes) {\n        return this._collabClientId ? serializeNode(node, mutatedNodes) : node;\n    }\n\n    unserializeNode(node) {\n        return this._collabClientId ? unserializeNode(node) : node;\n    }\n\n    automaticStepActive(label) {\n        this._observerTimeoutUnactive.delete(label);\n    }\n    automaticStepUnactive(label) {\n        this._observerTimeoutUnactive.add(label);\n    }\n    automaticStepSkipStack() {\n        this.automaticStepUnactive('skipStack');\n        setTimeout(() => this.automaticStepActive('skipStack'));\n    }\n    observerUnactive(label) {\n        this._observerUnactiveLabels.add(label);\n        if (this.observer) {\n            clearTimeout(this.observerTimeout);\n            this.observerFlush();\n            this.dispatchEvent(new Event('observerUnactive'));\n            this.observer.disconnect();\n        }\n    }\n    observerFlush() {\n        const records = this.observer.takeRecords();\n        this.observerIdSet(records);\n        this.observerApply(this.filterMutationRecords(records));\n    }\n    observerActive(label) {\n        this._observerUnactiveLabels.delete(label);\n        if (this._observerUnactiveLabels.size !== 0) return;\n\n        if (!this.observer) {\n            this.observer = new MutationObserver(records => {\n                this.observerIdSet(records);\n                records = this.filterMutationRecords(records);\n                if (!records.length) return;\n                this.dispatchEvent(new Event('contentChanged'));\n                clearTimeout(this.observerTimeout);\n                if (this._observerTimeoutUnactive.size === 0) {\n                    this.observerTimeout = setTimeout(() => {\n                        this.historyStep();\n                    }, 100);\n                }\n                this.observerApply(records);\n            });\n        }\n        this.dispatchEvent(new Event('preObserverActive'));\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        this.dispatchEvent(new Event('observerActive'));\n    }\n\n    observerIdSet(records) {\n        for (const record of records) {\n            if (record.type === 'childList') {\n                this.idSet(record.target);\n            }\n        }\n    }\n\n    observerApply(records) {\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                    this.idSet(node, this._checkStepUnbreakable);\n                    mutatedNodes.add(node.oid);\n                }\n                for (const node of record.removedNodes) {\n                    this.idSet(node, this._checkStepUnbreakable);\n                    mutatedNodes.delete(node.oid);\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': record.target.oid,\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': record.target.oid,\n                        'attributeName': record.attributeName,\n                        'value': record.target.getAttribute(record.attributeName),\n                        'oldValue': record.oldValue,\n                    });\n                    break;\n                }\n                case 'childList': {\n                    record.addedNodes.forEach(added => {\n                        if (!this._toRollback && containsUnremovable(added)) {\n                            this._toRollback = UNREMOVABLE_ROLLBACK_CODE;\n                        }\n                        const mutation = {\n                            'type': 'add',\n                        };\n                        if (!record.nextSibling && record.target.oid) {\n                            mutation.append = record.target.oid;\n                        } else if (record.nextSibling && record.nextSibling.oid) {\n                            mutation.before = record.nextSibling.oid;\n                        } else if (!record.previousSibling && record.target.oid) {\n                            mutation.prepend = record.target.oid;\n                        } else if (record.previousSibling && record.previousSibling.oid) {\n                            mutation.after = record.previousSibling.oid;\n                        } else {\n                            return false;\n                        }\n                        mutation.id = added.oid;\n                        mutation.node = this.serializeNode(added, mutatedNodes);\n                        this._currentStep.mutations.push(mutation);\n                    });\n                    record.removedNodes.forEach(removed => {\n                        if (!this._toRollback && containsUnremovable(removed)) {\n                            this._toRollback = UNREMOVABLE_ROLLBACK_CODE;\n                        }\n                        this._currentStep.mutations.push({\n                            'type': 'remove',\n                            'id': removed.oid,\n                            'parentId': record.target.oid,\n                            'node': this.serializeNode(removed),\n                            'nextId': record.nextSibling ? record.nextSibling.oid : undefined,\n                            'previousId': record.previousSibling\n                                ? record.previousSibling.oid\n                                : undefined,\n                        });\n                    });\n                    break;\n                }\n            }\n        }\n        if (records.length) {\n            this.dispatchEvent(new Event('observerApply'));\n        }\n    }\n    filterMutationRecords(records) {\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) continue;\n                if (record.attributeName === 'contenteditable') {\n                    continue;\n                }\n\n                attributeCache.set(record.target, attributeCache.get(record.target) || {});\n                if (record.attributeName === 'class') {\n                    const classBefore = (record.oldValue && record.oldValue.split(' ')) || [];\n                    const classAfter = (record.target.className && record.target.className.split && record.target.className.split(' ')) || [];\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 (excludedClasses.length && excludedClasses.every(c => this.options.renderingClasses.includes(c))) {\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            const closestProtectedCandidate = closestElement(record.target, '[data-oe-protected]');\n            if (closestProtectedCandidate) {\n                const protectedValue = closestProtectedCandidate.dataset.oeProtected;\n                switch (protectedValue) {\n                    case \"true\":\n                    case \"\":\n                        if (\n                            record.type !== \"attributes\" ||\n                            record.target !== closestProtectedCandidate ||\n                            isProtected(closestProtectedCandidate.parentElement)\n                        ) {\n                            continue;\n                        }\n                        break;\n                    case \"false\":\n                        if (\n                            record.type === \"attributes\" &&\n                            record.target === closestProtectedCandidate &&\n                            isProtected(closestProtectedCandidate.parentElement)\n                        ) {\n                            continue;\n                        }\n                        break;\n                }\n            }\n            filteredRecords.push(record);\n        }\n        return this.options.filterMutationRecords(filteredRecords);\n    }\n\n    // History\n    // -------------------------------------------------------------------------\n\n    historyReset() {\n        this._historyClean();\n        const firstStep = this._historyGetSnapshotStep();\n        this._firstStepId = firstStep.id;\n        this._historySnapshots = [{ step: firstStep }];\n        this._historySteps.push(firstStep);\n        // The historyIds carry the ids of the steps that were dropped when\n        // doing a snapshot.\n        // Those historyIds are used to compare if the last step saved in the\n        // server is present in the current historySteps or historyIds to\n        // ensure it is the same history branch.\n        this._historyIds = [];\n    }\n    /**\n     * Set the initial document history id.\n     *\n     * To prevent a saving a document with a diverging history, we store the\n     * last history id in the first node of the document to the database.\n     * This method provide the initial document history id to the editor.\n     */\n    historySetInitialId(id) {\n        this._historyIds.unshift(id);\n    }\n    /**\n     * Get all the history ids for the current history branch.\n     *\n     * See `_historyIds` in `historyReset`.\n     */\n    historyGetBranchIds() {\n        return this._historyIds.concat(this._historySteps.map(s => s.id));\n    }\n    historyGetSnapshotSteps() {\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 historyResetFromSteps).\n        if (!this._historySnapshots[0].time) {\n            return { steps: this._historySteps, historyIds: this.historyGetBranchIds() };\n        }\n        const steps = [];\n        let snapshot;\n        if (this._historySnapshots[0].time + HISTORY_SNAPSHOT_BUFFER_TIME < Date.now()) {\n            snapshot = this._historySnapshots[0];\n        } else {\n            // this._historySnapshots[1] has being created at least 1 minute ago\n            // (HISTORY_SNAPSHOT_INTERVAL) or it is the first step.\n            snapshot = this._historySnapshots[1];\n        }\n        let index = this._historySteps.length - 1;\n        while (this._historySteps[index].id !== snapshot.step.id) {\n            steps.push(this._historySteps[index]);\n            index--;\n        }\n        steps.push(snapshot.step);\n        steps.reverse();\n\n        return { steps, historyIds: this.historyGetBranchIds() };\n    }\n    historyResetFromSteps(steps, historyIds) {\n        this._historyIds = historyIds;\n        this.observerUnactive();\n        for (const node of [...this.editable.childNodes]) {\n            node.remove();\n        }\n        this._historyClean();\n        for (const step of steps) {\n            this.historyApply(step.mutations);\n        }\n        this._historySnapshots = [{ step: steps[0] }];\n        this._historySteps = steps;\n\n        this._postProcessExternalStepsPromise = this.options.postProcessExternalSteps(this.editable);\n\n        this._handleCommandHint();\n        this.multiselectionRefresh();\n        this.observerActive();\n        this.dispatchEvent(new Event('historyResetFromSteps'));\n    }\n    historyGetSteps() {\n        return this._historySteps;\n    }\n    historyGetMissingSteps({fromStepId, toStepId}) {\n        const fromIndex = this._historySteps.findIndex(x => x.id === fromStepId);\n        const toIndex = toStepId ? this._historySteps.findIndex(x => x.id === toStepId) : this._historySteps.length;\n        if (fromIndex === -1 || toIndex === -1) {\n            return -1;\n        }\n        return this._historySteps.slice(fromIndex + 1, toIndex);\n    }\n\n    // One step completed: apply to vDOM, setup next history step\n    historyStep(skipRollback = false, { stepId } = {}) {\n        if (!this._historyStepsActive) {\n            return;\n        }\n        this.sanitize();\n        // check that not two unBreakables modified\n        if (this._toRollback) {\n            if (!skipRollback) this.historyRollback();\n            this._toRollback = false;\n        }\n\n        // push history\n        const currentStep = this._currentStep;\n        if (!currentStep.mutations.length) {\n            return false;\n        }\n\n        currentStep.id = stepId || this._generateId();\n        const previousStep = peek(this._historySteps);\n        currentStep.clientId = this._collabClientId;\n        currentStep.previousStepId = previousStep.id;\n\n        this._historySteps.push(currentStep);\n        if (this.options.onHistoryStep) {\n            this.options.onHistoryStep(currentStep);\n        }\n        this._currentStep = {\n            selection: {},\n            mutations: [],\n        };\n        this._checkStepUnbreakable = true;\n        this._recordHistorySelection();\n        this.dispatchEvent(new Event('historyStep'));\n        this.multiselectionRefresh();\n    }\n    // apply changes according to some records\n    historyApply(records) {\n        for (const record of records) {\n            if (record.type === 'characterData') {\n                const node = this.idFind(record.id);\n                if (node) {\n                    node.textContent = record.text;\n                }\n            } else if (record.type === 'attributes') {\n                const node = this.idFind(record.id);\n                if (node) {\n                    let value = record.value;\n                    if (typeof value === 'string' && record.attributeName === 'class') {\n                        value = value.split(' ').filter(c => !this.options.renderingClasses.includes(c)).join(' ');\n                    }\n                    if (this._collabClientId) {\n                        this._safeSetAttribute(node, record.attributeName, value);\n                    } else {\n                        node.setAttribute(record.attributeName, value);\n                    }\n                }\n            } else if (record.type === 'remove') {\n                const toremove = this.idFind(record.id);\n                if (toremove) {\n                    toremove.remove();\n                }\n            } else if (record.type === 'add') {\n                let node = this.idFind(record.oid) || (record.node && this.unserializeNode(record.node));\n                if (!node) {\n                    continue;\n                }\n                if (this._collabClientId) {\n                    const fakeNode = document.createElement('fake-el');\n                    fakeNode.appendChild(node);\n                    DOMPurify.sanitize(fakeNode, {\n                        IN_PLACE: true,\n                        ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n                        ADD_ATTR: [\"contenteditable\"],\n                    });\n                    node = fakeNode.childNodes[0];\n                    if (!node) {\n                        continue;\n                    }\n                }\n\n                this.idSet(node, true);\n\n                if (record.append && this.idFind(record.append)) {\n                    this.idFind(record.append).append(node);\n                } else if (record.before && this.idFind(record.before)) {\n                    this.idFind(record.before).before(node);\n                } else if (record.after && this.idFind(record.after)) {\n                    this.idFind(record.after).after(node);\n                } else {\n                    continue;\n                }\n            }\n        }\n    }\n    historyRollback(until = 0) {\n        const step = this._currentStep;\n        this.observerFlush();\n        this.historyRevert(step, { until });\n        this.observerFlush();\n        step.mutations = step.mutations.slice(0, until);\n        this._toRollback = false;\n    }\n    /**\n     * Undo the current non-recorded draft step.\n     */\n    historyRevertCurrentStep() {\n        this.observerFlush();\n        this.historyRevert(this._currentStep, {sideEffect: false});\n        this.observerFlush();\n        // Clear current step from all previous changes.\n        this._currentStep.mutations = [];\n\n        this.activateContenteditable();\n        this.historySetSelection(this._currentStep);\n    }\n    /**\n     * Undo a step of the history.\n     *\n     * this._historyStepsState is a map from it's location (index) in this.history to a state.\n     * The state can be on of:\n     * undefined: the position has never been undo or redo.\n     * \"redo\": The position is considered as a redo of another.\n     * \"undo\": The position is considered as a undo of another.\n     * \"consumed\": The position has been undone and is considered consumed.\n     */\n    historyUndo() {\n        this.options.preHistoryUndo();\n        // The last step is considered an uncommited draft so always revert it.\n        const lastStep = this._currentStep;\n        this.historyRevert(lastStep);\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._historyStepsStates.set(this._historySteps[pos].id, 'consumed');\n            this.historyRevert(this._historySteps[pos]);\n            // Consider the last position of the history as an undo.\n            const stepId = this._generateId();\n            this._historyStepsStates.set(stepId, 'undo');\n            this.historyStep(true, { stepId });\n            this.dispatchEvent(new Event('historyUndo'));\n        }\n    }\n    /**\n     * Redo a step of the history.\n     *\n     * @see historyUndo\n     */\n    historyRedo() {\n        // Current step is considered an uncommitted draft, so revert it,\n        // otherwise a redo would not be possible.\n        this.historyRevert(this._currentStep);\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._historyStepsStates.set(this._historySteps[pos].id, 'consumed');\n            this.historyRevert(this._historySteps[pos]);\n            this.historySetSelection(this._historySteps[pos]);\n            const stepId = this._generateId();\n            this._historyStepsStates.set(stepId, 'redo');\n            this.historyStep(true, { stepId });\n            this.dispatchEvent(new Event('historyRedo'));\n        }\n    }\n    /**\n     * Check wether undoing is possible.\n     */\n    historyCanUndo() {\n        return this._getNextUndoIndex() > 0;\n    }\n    /**\n     * Check wether redoing is possible.\n     */\n    historyCanRedo() {\n        return this._getNextRedoIndex() > 0;\n    }\n    historySize() {\n        return this._historySteps.length;\n    }\n\n    historyRevert(step, { until = 0, sideEffect = true } = {} ) {\n        // apply dom changes by reverting history steps\n        for (let i = step.mutations.length - 1; i >= until; i--) {\n            const mutation = step.mutations[i];\n            if (!mutation) {\n                break;\n            }\n            switch (mutation.type) {\n                case 'characterData': {\n                    const node = this.idFind(mutation.id);\n                    if (node) node.textContent = mutation.oldValue;\n                    break;\n                }\n                case 'attributes': {\n                    const node = this.idFind(mutation.id);\n                    if (node) {\n                        if (mutation.oldValue) {\n                            let value = mutation.oldValue;\n                            if (typeof value === 'string' && mutation.attributeName === 'class') {\n                                value = value.split(' ').filter(c => !this.options.renderingClasses.includes(c)).join(' ');\n                            }\n                            if (this._collabClientId) {\n                                this._safeSetAttribute(node, mutation.attributeName, value);\n                            } else {\n                                node.setAttribute(mutation.attributeName, value);\n                            }\n                        } else {\n                            node.removeAttribute(mutation.attributeName);\n                        }\n                    }\n                    break;\n                }\n                case 'remove': {\n                    let nodeToRemove = this.idFind(mutation.id);\n                    if (!nodeToRemove) {\n                        if (!mutation.node) {\n                            continue;\n                        }\n                        nodeToRemove = this.unserializeNode(mutation.node);\n                        const fakeNode = document.createElement('fake-el');\n                        fakeNode.appendChild(nodeToRemove);\n                        DOMPurify.sanitize(fakeNode, {\n                            IN_PLACE: true,\n                            ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n                            ADD_ATTR: [\"contenteditable\"],\n                        });\n                        nodeToRemove = fakeNode.childNodes[0];\n                        if (!nodeToRemove) {\n                            continue;\n                        }\n                        this.idSet(nodeToRemove);\n                    }\n                    if (mutation.nextId && this.idFind(mutation.nextId)?.isConnected) {\n                        const node = this.idFind(mutation.nextId);\n                        node && node.before(nodeToRemove);\n                    } else if (mutation.previousId && this.idFind(mutation.previousId)?.isConnected) {\n                        const node = this.idFind(mutation.previousId);\n                        node && node.after(nodeToRemove);\n                    } else {\n                        const node = this.idFind(mutation.parentId);\n                        node && node.append(nodeToRemove);\n                    }\n                    break;\n                }\n                case 'add': {\n                    const node = this.idFind(mutation.id);\n                    if (node) {\n                        node.remove();\n                        node.ouid = undefined;\n                    }\n                }\n            }\n        }\n        if (sideEffect) {\n            this.historySetSelection(step);\n        }\n    }\n    /**\n     * Ensure that a callback is called without triggering a rollback.\n     *\n     * If a rollback was set before the callback, do not reset it.\n     */\n    withoutRollback(callback) {\n        const priorRollback = this._toRollback;\n        callback();\n        this.observerFlush();\n        if (!priorRollback) {\n            this._toRollback = false;\n        }\n    }\n    /**\n     * Place the selection on the last known selection position from the history\n     * steps.\n     *\n     * @param {boolean} [limitToEditable=false] When true returns the latest selection that\n     *     happened within the editable.\n     * @returns {boolean}\n     */\n    historyResetLatestComputedSelection(limitToEditable) {\n        const computedSelection = limitToEditable\n            ? this._latestComputedSelectionInEditable\n            : this._latestComputedSelection;\n        if (computedSelection && computedSelection.anchorNode) {\n            const anchorNode = this.idFind(computedSelection.anchorNode.oid);\n            const focusNode = this.idFind(computedSelection.focusNode.oid) || anchorNode;\n            if (anchorNode) {\n                setSelection(\n                    anchorNode,\n                    computedSelection.anchorOffset,\n                    focusNode,\n                    computedSelection.focusOffset,\n                );\n                return true;\n            }\n        }\n        return false;\n    }\n    historySetSelection(step) {\n        if (step.selection && step.selection.anchorNodeOid) {\n            const anchorNode = this.idFind(step.selection.anchorNodeOid);\n            const focusNode = this.idFind(step.selection.focusNodeOid) || anchorNode;\n            if (anchorNode) {\n                setSelection(\n                    anchorNode,\n                    step.selection.anchorOffset,\n                    focusNode,\n                    step.selection.focusOffset !== undefined\n                        ? step.selection.focusOffset\n                        : step.selection.anchorOffset,\n                    false,\n                );\n                // If a table must be selected, ensure it's in the same tick.\n                this._handleSelectionInTable();\n            }\n        }\n    }\n    unbreakableStepUnactive() {\n        if (this._toRollback === UNBREAKABLE_ROLLBACK_CODE) {\n            this._toRollback = false;\n        }\n        this._checkStepUnbreakable = false;\n    }\n    historyPauseSteps() {\n        this._historyStepsActive = false;\n    }\n    historyUnpauseSteps() {\n        this._historyStepsActive = true;\n    }\n    /**\n     * Stash the mutations of the current step to re-apply them later.\n     */\n    historyStash() {\n        if (!this._historyStashedMutations) {\n            this._historyStashedMutations = [];\n        }\n        this._historyStashedMutations.push(...this._currentStep.mutations);\n        this._currentStep.mutations = [];\n    }\n    /**\n     * Unstash the previously stashed mutations into the current step.\n     */\n    historyUnstash() {\n        if (!this._currentStep.mutations) {\n            this._currentStep.mutations = [];\n        }\n        this._currentStep.mutations.unshift(...this._historyStashedMutations);\n        this._historyStashedMutations = [];\n    }\n    _historyClean() {\n        this._historySteps = [];\n        this._currentStep = {\n            selection: {\n                anchorNodeOid: undefined,\n                anchorOffset: undefined,\n                focusNodeOid: undefined,\n                focusOffset: undefined,\n            },\n            mutations: [],\n            id: undefined,\n            clientId: undefined,\n        };\n        this._historyStepsStates = new Map();\n    }\n    _historyGetSnapshotStep() {\n        return {\n            selection: {\n                anchorNode: undefined,\n                anchorOffset: undefined,\n                focusNode: undefined,\n                focusOffset: undefined,\n            },\n            mutations: Array.from(this.editable.childNodes).map(node => ({\n                type: 'add',\n                append: 1,\n                id: node.oid,\n                node: this.serializeNode(node),\n            })),\n            id: this._generateId(),\n            clientId: this.clientId,\n            previousStepId: undefined,\n        };\n    }\n    _historyMakeSnapshot() {\n        if (\n            !this._lastSnapshotHistoryLength ||\n            this._lastSnapshotHistoryLength < this._historySteps.length\n        ) {\n            this._lastSnapshotHistoryLength = this._historySteps.length;\n            const step = this._historyGetSnapshotStep();\n            step.id = this._historySteps[this._historySteps.length - 1].id;\n            const snapshot = {\n                time: Date.now(),\n                step: step,\n            };\n            this._historySnapshots = [snapshot, this._historySnapshots[0]];\n        }\n    }\n    /**\n     * Insert a step from another collaborator.\n     */\n    _historyAddExternalStep(newStep) {\n        let index = this._historySteps.length - 1;\n        while (index >= 0 && this._historySteps[index].id !== newStep.previousStepId) {\n            // Skip steps that are already in the list.\n            if (this._historySteps[index].id === newStep.id) {\n                return;\n            }\n            index--;\n        }\n\n        // When the previousStepId is not present in the this._historySteps it\n        // could be either:\n        // - the previousStepId is before a snapshot of the same history\n        // - the previousStepId has not been received because clients were\n        //   disconnected at that time\n        // - the previousStepId is in another history (in case two totally\n        //   differents this._historySteps (but it should not arise)).\n        if (index < 0) {\n            if (this.options.onHistoryMissingParentSteps) {\n                const historySteps = this._historySteps;\n                let index = historySteps.length - 1;\n                // Get the last known step that we are sure the missing step\n                // client has. It could either be a step that has the same\n                // clientId or the first step.\n                while(index !== 0) {\n                    if (historySteps[index].clientId === newStep.clientId) {\n                        break;\n                    }\n                    index--;\n                }\n                const fromStepId = historySteps[index].id;\n                this.options.onHistoryMissingParentSteps({\n                    step: newStep,\n                    fromStepId: fromStepId,\n                });\n            }\n            return;\n        }\n\n        let concurentSteps = [];\n        index++;\n        while (index < this._historySteps.length) {\n            if (this._historySteps[index].previousStepId === newStep.previousStepId) {\n                if (this._historySteps[index].id.localeCompare(newStep.id) === 1) {\n                    break;\n                } else {\n                    concurentSteps = [this._historySteps[index].id];\n                }\n            } else {\n                if (concurentSteps.includes(this._historySteps[index].previousStepId)) {\n                    concurentSteps.push(this._historySteps[index].id);\n                } else {\n                    break;\n                }\n            }\n            index++;\n        }\n\n        const stepsAfterNewStep = this._historySteps.slice(index);\n\n        for (const stepToRevert of stepsAfterNewStep.slice().reverse()) {\n            this.historyRevert(stepToRevert, { sideEffect: false });\n        }\n        this.historyApply(newStep.mutations);\n        this._historySteps.splice(index, 0, newStep);\n        for (const stepToApply of stepsAfterNewStep) {\n            this.historyApply(stepToApply.mutations);\n        }\n    }\n    collaborationSetClientId(id) {\n        this._collabClientId = id;\n    }\n\n    /**\n     * Apply external steps coming from the collaboration. Buffer them if\n     * _postProcessExternalStepsPromise is not null until it is resolved (since\n     * steps could potentially concern elements currently being rendered\n     * asynchronously).\n     *\n     * @param {Object} newSteps External steps to be applied\n     */\n    onExternalHistorySteps(newSteps) {\n        if (this._postProcessExternalStepsPromise) {\n            this._externalStepsBuffer.push(...newSteps);\n        }\n        this.observerUnactive();\n        this._computeHistorySelection();\n\n        let stepIndex = 0;\n        for (const newStep of newSteps) {\n            this._historyAddExternalStep(newStep);\n            stepIndex++;\n            this._postProcessExternalStepsPromise = this.options.postProcessExternalSteps(this.editable);\n            if (this._postProcessExternalStepsPromise) {\n                this._postProcessExternalStepsPromise.then(() => {\n                    this._postProcessExternalStepsPromise = undefined;\n                    this.onExternalHistorySteps(this._externalStepsBuffer);\n                });\n                this._externalStepsBuffer = newSteps.slice(stepIndex);\n                break;\n            }\n        }\n\n        this.observerActive();\n        this.historyResetLatestComputedSelection();\n        this._handleCommandHint();\n        this.multiselectionRefresh();\n        this.dispatchEvent(new Event('onExternalHistorySteps'));\n    }\n\n    // Multi selection\n    // -------------------------------------------------------------------------\n\n    onExternalMultiselectionUpdate(selection) {\n        const { clientId } = selection;\n        const currentInfo = this._collabSelectionInfos.get(clientId);\n        if (currentInfo) {\n            currentInfo.selection = selection;\n        } else {\n            this._collabSelectionInfos.set(clientId, { selection });\n        }\n        this._drawClientSelection(selection);\n        this._drawClientAvatar(selection);\n        this._updateAvatarCounters();\n    }\n\n    multiselectionRefresh() {\n        for (const { selection } of this._collabSelectionInfos.values()) {\n            this._drawClientSelection(selection);\n            this._drawClientAvatar(selection);\n        }\n        this._updateAvatarCounters();\n    }\n\n    _drawClientSelection({ selection, color, clientId, clientName = this.options._t('Anonymous') }) {\n        this._multiselectionRemoveClient(clientId);\n        let clientRects;\n\n        let anchorNode = this.idFind(selection.anchorNodeOid);\n        let focusNode = this.idFind(selection.focusNodeOid);\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\n        if (anchorNode.isConnected && focusNode.isConnected) {\n            [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n            [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\n        } else {\n            // TODO: This is a stable fix for drawing an incorrect selection in\n            // a niche case. The root cause will be fixed in master.\n            anchorNode = this.editable.children[0];\n            focusNode = this.editable.children[0];\n            anchorOffset = 0;\n            focusOffset = 0;\n        }\n\n        const direction = getCursorDirection(\n            anchorNode,\n            anchorOffset,\n            focusNode,\n            focusOffset,\n        );\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._selectionsContainer.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: ${color};\n                opacity: 0.25;\n                pointer-events: none;\n            `;\n            rectElement.setAttribute('data-selection-client-id', clientId);\n            return rectElement;\n        });\n\n        // Draw carret.\n        const caretElement = this.document.createElement('div');\n        caretElement.style = `border-left: 2px solid ${color}; position: absolute;`;\n        caretElement.setAttribute('data-selection-client-id', clientId);\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'] = color;\n        caretTopSquare.setAttribute('data-client-name', clientName);\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 = peek(clientRects);\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._selectionsContainer.append(caretElement, ...indicators);\n    }\n\n    _drawClientAvatar({ selection, clientId, clientAvatarUrl = '', clientName = this.options._t('Anonymous') }) {\n        const anchorNode = this.idFind(selection.anchorNodeOid);\n        const focusNode = this.idFind(selection.focusNodeOid);\n        if (!anchorNode || !focusNode) {\n            return;\n        }\n        const anchorBlock = closestBlock(anchorNode);\n        if (!anchorBlock) return;\n\n        const containerRect = this._avatarsContainer.getBoundingClientRect();\n\n        // Draw user avatar.\n        const selectionInfo = this._collabSelectionInfos.get(clientId) || {};\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', clientAvatarUrl);\n            image.classList.add('o_object_fit_cover');\n        }\n        // Avoid re-appending the element in the dom.\n        if (!avatarElement.parentElement) {\n            this._avatarsContainer.append(avatarElement);\n        }\n        // Make sure data is up to date.\n        selectionInfo.avatarElement = avatarElement;\n        selectionInfo.clientName = clientName;\n        selectionInfo.avatarTargetElement = anchorBlock;\n        this._collabSelectionInfos.set(clientId, 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\n    _updateAvatarCounters() {\n        this._avatarsOverlaps = {};\n        for (const info of this._collabSelectionInfos.values()) {\n            const key =  info.avatarPositionKey;\n            this._avatarsOverlaps[key] = this._avatarsOverlaps[key] || new Set();\n            this._avatarsOverlaps[key].add(info);\n        }\n\n        // Render avatars overlap.\n        this._avatarsCountersContainer.replaceChildren();\n        for (const [overlapKey, infos] of Object.entries(this._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._avatarsCountersContainer.append(div);\n            }\n        }\n    }\n\n    multiselectionRemove(clientId) {\n        const selectionInfo = this._collabSelectionInfos.get(clientId);\n        if (selectionInfo && selectionInfo.avatarElement) {\n            selectionInfo.avatarElement.remove();\n        }\n        this._multiselectionRemoveClient(clientId)\n        this._collabSelectionInfos.delete(clientId);\n        this._updateAvatarCounters();\n    }\n\n    _multiselectionRemoveClient(clientId) {\n        const elements = this._selectionsContainer.querySelectorAll(\n            `[data-selection-client-id=\"${clientId}\"]`,\n        );\n        for (const element of elements) {\n            element.remove();\n        }\n    }\n\n    /**\n     * Same as @see _applyCommand, except that also simulates all the\n     * contenteditable behaviors we let happen, e.g. the backspace handling\n     * we then rollback.\n     *\n     * TODO this uses document.execCommand (which is deprecated) and relies on\n     * the fact that using a command through it leads to the same result as\n     * executing that command through a user keyboard on the unaltered editable\n     * section with standard contenteditable attribute. This is already a huge\n     * assomption.\n     *\n     * @param {string} method\n     * @returns {?}\n     */\n    execCommand(...args) {\n        this._computeHistorySelection();\n        return this._applyCommand(...args);\n    }\n\n    /**\n     * Find all descendants of `element` with a `data-call` attribute and bind\n     * them on click to the execution of the command matching that\n     * attribute.\n     */\n    bindExecCommand(element) {\n        for (const buttonEl of element.querySelectorAll('[data-call]')) {\n            buttonEl.addEventListener('click', ev => {\n                const arg1 = buttonEl.dataset.arg1;\n                const args = arg1 && arg1.split(\",\") || [];\n                this.execCommand(buttonEl.dataset.call, ...args);\n\n                this.historyResetLatestComputedSelection(true);\n                ev.preventDefault();\n                this._updateToolbar();\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() {\n        const tds = this.editable.querySelectorAll('.o_selected_table, .o_selected_td');\n        if (!tds.length) {\n            return false;\n        }\n        this.observerUnactive('deselectTable');\n        for (const td of tds) {\n            td.classList.remove('o_selected_td', 'o_selected_table');\n            if (!td.classList.length) {\n                td.removeAttribute('class');\n            }\n        }\n        this.observerActive('deselectTable');\n        return true;\n    }\n\n    /**\n     * `activateContenteditable` serves as an interface for external use,\n     * allowing users to conveniently trigger `_activateContenteditable`\n     * from outside the odooEditor.\n     */\n    activateContenteditable() {\n        this.canActivateContentEditable = true;\n        this._activateContenteditable();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _removeDomListener() {\n        for (const [element, eventName, boundCallback] of this._domListeners) {\n            element.removeEventListener(eventName, boundCallback);\n        }\n        this._domListeners = [];\n    }\n\n    // EDITOR COMMANDS\n    // ===============\n\n    deleteRange(sel) {\n        if (this.deleteTableRange()) {\n            return;\n        }\n        // Remove all FEFF text nodes\n        let range = getDeepRange(this.editable, { sel, correctTripleClick: true });\n        if (!range) return;\n        for (const node of descendants(closestBlock(range.commonAncestorContainer))) {\n            if (node.nodeType === Node.TEXT_NODE && [...node.textContent].every(char => char === '\\uFEFF')) {\n                const restore = prepareUpdate(...leftPos(node));\n                node.remove();\n                restore(); // Make sure to make <br>s visible if needed.\n            }\n        }\n\n        // we get the `columnsContainer` (.o_text_columns) in case the user added columns and is deleting them \n        const columnsContainers = [];\n        const fullRange = this.document.getSelection().getRangeAt(0);\n        const selectionCommonAncestor = fullRange.commonAncestorContainer;\n        if (selectionCommonAncestor.nodeType === Node.ELEMENT_NODE) {\n            const rows = selectionCommonAncestor.classList.contains(\"row\")\n                ? [selectionCommonAncestor]\n                : selectionCommonAncestor.getElementsByClassName(\"row\");\n            for (const row of rows) {\n                if (\n                    row &&\n                    row.parentElement &&\n                    row.parentElement.classList.contains(\"o_text_columns\")\n                ) {\n                    const firstColumnNode = firstLeaf(row);\n                    const lastColumnNode = lastLeaf(row);\n                    if (\n                        fullRange.isPointInRange(firstColumnNode, 0) &&\n                        fullRange.isPointInRange(lastColumnNode, 0)\n                    ) {\n                        columnsContainers.push(row.parentElement);\n                    }\n                }\n            }\n        }\n\n        if (!this.editable.childElementCount) {\n            // Ensure the editable has content.\n            const p = document.createElement('p');\n            p.append(document.createElement('br'));\n            this.editable.append(p);\n            setSelection(p, 0);\n            return;\n        }\n        range = getDeepRange(this.editable, {\n            sel,\n            splitText: true,\n            select: true,\n            correctTripleClick: true,\n        });\n        if (!range) return;\n        // Expand the range to fully include all contentEditable=False elements.\n        const commonAncestorContainer = this.editable.contains(range.commonAncestorContainer) ?\n            range.commonAncestorContainer :\n            this.editable;\n        const startUneditable = getFurthestUneditableParent(range.startContainer, commonAncestorContainer);\n        if (startUneditable) {\n            let leaf = previousLeaf(startUneditable);\n            if (leaf) {\n                range.setStart(leaf, nodeSize(leaf));\n            } else {\n                range.setStart(commonAncestorContainer, 0);\n            }\n        }\n        const endUneditable = getFurthestUneditableParent(range.endContainer, commonAncestorContainer);\n        if (endUneditable) {\n            let leaf = nextLeaf(endUneditable);\n            if (leaf) {\n                range.setEnd(leaf, 0);\n            } else {\n                range.setEnd(commonAncestorContainer, nodeSize(commonAncestorContainer));\n            }\n        }\n        let insertedZws;\n        let { startContainer: start, startOffset, endContainer: end, endOffset } = range;\n        const startBlock = closestBlock(start);\n        const endBlock = closestBlock(end);\n        const [firstLeafOfStartBlock, lastLeafOfEndBlock] = [firstLeaf(startBlock), lastLeaf(endBlock)];\n        const startLink = closestElement(range.startContainer, 'a');\n        const rangeStartSameAsColumnsStart =\n            columnsContainers.length &&\n            firstLeaf(range.startContainer) === firstLeaf(columnsContainers[0]);\n        if (\n            sel &&\n            !sel.isCollapsed &&\n            !range.startOffset &&\n            !range.startContainer.previousSibling &&\n            !startLink &&\n            !rangeStartSameAsColumnsStart // if the start is same as columns start we don't add `zws`\n        ) {\n            // Insert a zero-width space before the selection if the selection\n            // is non-collapsed and at the beginning of its parent, so said\n            // parent will have content after extraction. This ensures that the\n            // parent will not be removed by \"tricking\" `range.extractContents`.\n            // Eg, <h1><font>[...]</font></h1> will preserve the styles of the\n            // <font> node. If it remains empty, it will be cleaned up later by\n            // the sanitizer.\n            // Links are excluded from this.\n            const zws = document.createTextNode('\\u200B');\n            range.startContainer.before(zws);\n            insertedZws = zws;\n        }\n        // Do not join blocks in the following cases:\n        // 1. start and end share a common ancestor block with the range\n        // 2. selection spans multiple TDs\n        // 3. selection starts at beginning of startBlock and ends at end of\n        //    endBlock\n        const doJoin =\n            !(startBlock === closestBlock(range.commonAncestorContainer) &&\n                endBlock === closestBlock(range.commonAncestorContainer))\n            && (startBlock.tagName !== 'TD' && endBlock.tagName !== 'TD')\n            && !(firstLeafOfStartBlock === start && lastLeafOfEndBlock === end);\n        let next = nextLeaf(end, this.editable);\n\n        // Get the boundaries of the range so as to get the state to restore.\n        if (end.nodeType === Node.TEXT_NODE) {\n            splitTextNode(end, endOffset);\n            endOffset = nodeSize(end);\n        }\n        if (start.nodeType === Node.TEXT_NODE) {\n            splitTextNode(start, startOffset);\n            startOffset = 0;\n        }\n        const restoreUpdate = prepareUpdate(\n            ...boundariesOut(start).slice(0, 2),\n            ...boundariesOut(end).slice(2, 4),\n            { allowReenter: false, label: 'deleteRange' });\n\n        // handle the case when we select the columns (all) and only the columns\n        // we adjust the selection to cover the whole columnsContainers\n        if (columnsContainers.length) {\n            const firstColumnContainer = columnsContainers[0];\n            const lastColumnContainer = columnsContainers[columnsContainers.length - 1];\n            const startsWithColumn = firstLeaf(range.startContainer) === firstLeaf(firstColumnContainer);\n            const endsWithColumn = lastLeaf(range.endContainer) === lastLeaf(lastColumnContainer);\n            if (startsWithColumn) {\n                range.setStart(firstColumnContainer, 0);\n            }\n            if (endsWithColumn) {\n                range.setEnd(lastColumnContainer, lastColumnContainer.childNodes.length);\n            }\n        }\n\n        // Let the DOM split and delete the range.\n        const contents = range.extractContents();\n\n        // if our selection is at exactly the start and end of `columnsContainer`\n        // all its content will be removed but the parent will remain so we remove it manually\n        for (const columnsContainer of columnsContainers) {\n            if (!columnsContainer.hasChildNodes()) {\n                columnsContainer.remove();\n            }\n        }\n\n        setSelection(start, nodeSize(start));\n        const startLi = closestElement(start, 'li');\n        // Uncheck a list item with empty text in multi-list selection.\n        if (startLi && startLi.classList.contains('o_checked') &&\n            ['\\u200B', ''].includes(startLi.textContent) && closestElement(end, 'li') !== startLi) {\n            startLi.classList.remove('o_checked');\n        }\n        range = getDeepRange(this.editable, { sel });\n        // Restore unremovables removed by extractContents.\n        [...contents.querySelectorAll('*')].filter(isUnremovable).forEach(n => {\n            closestBlock(range.endContainer).after(n);\n            n.textContent = '';\n        });\n        // If the end container was fully selected, extractContents may have\n        // emptied it without removing it. Ensure it's gone.\n        const isRemovableInvisible = node =>\n            !isVisible(node) && !isZWS(node) && !isUnremovable(node);\n        const endIsStart = end === start;\n        while (end && isRemovableInvisible(end) && !end.contains(range.endContainer)) {\n            const parent = end.parentNode;\n            end.remove();\n            end = parent;\n        }\n        // Same with the start container\n        while (\n            start &&\n            !isBlock(start) && isRemovableInvisible(start) &&\n            !(endIsStart && start.contains(range.startContainer))\n        ) {\n            const parent = start.parentNode;\n            start.remove();\n            start = parent;\n        }\n        // Ensure empty blocks be given a <br> child.\n        if (start) {\n            if (start === this.editable && startBlock.textContent === '\\u200B') {\n                const p = document.createElement('p');\n                start.appendChild(p);\n                start = p;\n            }\n            fillEmpty(closestBlock(start));\n        }\n        fillEmpty(closestBlock(range.endContainer));\n        range = getDeepRange(this.editable, { sel });\n        let joinWith = range.endContainer;\n        const rightLeaf = rightLeafOnlyNotBlockPath(joinWith).next().value;\n        if (rightLeaf && rightLeaf.nodeValue === ' ') {\n            joinWith = rightLeaf;\n        }\n        // Rejoin blocks that extractContents may have split in two.\n        while (\n            doJoin &&\n            next &&\n            !(next.previousSibling && next.previousSibling === joinWith) &&\n            this.editable.contains(next) && (closestElement(joinWith,'TD') === closestElement(next, 'TD'))\n        ) {\n            const restore = preserveCursor(this.document);\n            this.observerFlush();\n            const res = this._protect(() => {\n                next.oDeleteBackward();\n                if (!this.editable.contains(joinWith)) {\n                    this._toRollback = UNREMOVABLE_ROLLBACK_CODE; // tried to delete too far -> roll it back.\n                } else {\n                    next = firstLeaf(next);\n                }\n            }, this._currentStep.mutations.length);\n            if ([UNBREAKABLE_ROLLBACK_CODE, UNREMOVABLE_ROLLBACK_CODE].includes(res)) {\n                restore();\n                break;\n            }\n        }\n        // If the oDeleteBackward loop emptied the start block and the range\n        // ends in another element (rangeStart !== rangeEnd), we delete the\n        // start block and move the cursor to the end block.\n        if (\n            startBlock &&\n            startBlock.textContent === '\\u200B' &&\n            endBlock &&\n            startBlock !== endBlock &&\n            !isEmptyBlock(endBlock) &&\n            paragraphRelatedElements.includes(endBlock.nodeName)\n        ) {\n            startBlock.remove();\n            setSelection(endBlock, 0);\n            fillEmpty(endBlock);\n        }\n        if (insertedZws) {\n            // Remove the zero-width space (zws) that was added to preserve the\n            // parent styles, then call `fillEmpty` to properly add a flagged\n            // zws if still needed.\n            const el = closestElement(insertedZws);\n            const next = insertedZws.nextSibling;\n            insertedZws.remove();\n            el && fillEmpty(el);\n            setSelection(next, 0);\n        }\n        if (joinWith) {\n            const el = closestElement(joinWith);\n            el && fillEmpty(el);\n        }\n        const restoreCursor = preserveCursor(this.document);\n        restoreUpdate();\n        restoreCursor();\n    }\n\n    /**\n     * Handle range deletion in cases that involve custom table selections.\n     * Return true if nodes removed _inside_ a table, false otherwise (or if the\n     * table itself was removed).\n     *\n     * @param {Selection} sel\n     * @returns {boolean}\n     */\n    deleteTableRange() {\n        const selectedTds = this.editable.querySelectorAll('.o_selected_td');\n        const fullySelectedTables = [...this.editable.querySelectorAll('.o_selected_table')].filter(table => (\n            [...table.querySelectorAll('td')].every(td => td.classList.contains('o_selected_td'))\n        ));\n        if (selectedTds.length && !fullySelectedTables.length) {\n            this.historyPauseSteps();\n            // A selection within a table has to be handled differently so it\n            // takes into account the custom table cell selections, and doesn't\n            // break the table. If the selection includes a table cell but also\n            // elements that are out of a table, the whole table will be\n            // selected so its deletion can be handled separately.\n            const rows = [...closestElement(selectedTds[0], 'tr').parentElement.children].filter(child => child.nodeName === 'TR');\n            const firstRowCells = [...rows[0].children].filter(child => child.nodeName === 'TD' || child.nodeName === 'TH');\n            const areFullColumnsSelected = getRowIndex(selectedTds[0]) === 0 && getRowIndex(selectedTds[selectedTds.length - 1]) === rows.length - 1;\n            const areFullRowsSelected = getColumnIndex(selectedTds[0]) === 0 && getColumnIndex(selectedTds[selectedTds.length - 1]) === firstRowCells.length - 1;\n            if (areFullColumnsSelected || areFullRowsSelected) {\n                // If some full columns are selected, remove them.\n                if (areFullColumnsSelected) {\n                    const startIndex = getColumnIndex(selectedTds[0]);\n                    let endIndex = getColumnIndex(selectedTds[selectedTds.length - 1]);\n                    let currentIndex = startIndex;\n                    while (currentIndex <= endIndex) {\n                        this.execCommand('removeColumn', firstRowCells[currentIndex]);\n                        currentIndex++;\n                    }\n                }\n                // If some full rows are selected, remove them.\n                if (areFullRowsSelected) {\n                    const startIndex = getRowIndex(selectedTds[0]);\n                    let endIndex = getRowIndex(selectedTds[selectedTds.length - 1]);\n                    let currentIndex = startIndex;\n                    while (currentIndex <= endIndex) {\n                        this.execCommand('removeRow', rows[currentIndex]);\n                        currentIndex++;\n                    }\n                }\n            } else {\n                // If no full row or column is selected, empty the selected cells.\n                for (const td of selectedTds) {\n                    [...td.childNodes].forEach(child => child.remove());\n                    td.append(document.createElement('br'));\n                }\n            }\n            this.historyUnpauseSteps();\n            this.historyStep();\n            return true;\n        } else if (fullySelectedTables.length) {\n            fullySelectedTables.forEach(table => table.remove());\n        }\n        this._toggleTableUi();\n        return false;\n    }\n\n    /**\n     * Displays the text colors (foreground ink and background highlight)\n     * based on the current text cursor position. For gradients, displays\n     * the average color of the gradient.\n     *\n     * @param {object} [params]\n     * @param {string} [params.foreColor] - forces the 'foreColor' in the\n     *     toolbar instead of determining it from the cursor position\n     * @param {string} [params.hiliteColor] - forces the 'hiliteColor' in the\n     *     toolbar instead of determining it from the cursor position\n     */\n    updateColorpickerLabels(params = {}) {\n        function hexFromColor(color) {\n            if (isColorGradient(color)) {\n                // For gradients, compute the average color\n                color = color.match(/gradient(.*)/)[0];\n                let r = 0, g = 0, b = 0, count = 0;\n                for (const entry of color.matchAll(/rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+)(?:,\\s*(\\d+(?:\\.\\d+)?))?\\)/g)) {\n                    count++;\n                    r += parseInt(entry[1], 10);\n                    g += parseInt(entry[2], 10);\n                    b += parseInt(entry[3], 10);\n                }\n                color = `rgb(${Math.round(r / count)}, ${Math.round(g / count)}, ${Math.round(b / count)})`;\n            }\n            return rgbToHex(color);\n        }\n        let foreColor = params.foreColor;\n        let hiliteColor = params.hiliteColor;\n\n        // Determine colors at cursor position\n        const sel = this.document.getSelection();\n        if (sel.rangeCount && (!foreColor || !hiliteColor)) {\n            const endContainer = closestElement(sel.getRangeAt(0).endContainer);\n            const computedStyle = getComputedStyle(endContainer);\n            const backgroundImage = computedStyle.backgroundImage;\n            const hasGradient = isColorGradient(backgroundImage);\n            const hasTextGradientClass = endContainer.classList.contains('text-gradient');\n            if (!foreColor) {\n                if (hasGradient && hasTextGradientClass) {\n                    foreColor = backgroundImage;\n                } else {\n                    foreColor = this.document.queryCommandValue('foreColor');\n                }\n            }\n            if (!hiliteColor) {\n                if (hasGradient && !hasTextGradientClass) {\n                    hiliteColor = backgroundImage;\n                } else {\n                    let ancestor = endContainer;\n                    while (ancestor && !hiliteColor) {\n                        hiliteColor = ancestor.style.backgroundColor;\n                        ancestor = ancestor.parentElement;\n                    }\n                    if (!hiliteColor) {\n                        hiliteColor = this.document.queryCommandValue('backColor');\n                    }\n                }\n            }\n        }\n\n        // display colors in toolbar buttons\n        foreColor = hexFromColor(foreColor);\n        this.toolbar.style.setProperty('--fore-color', foreColor);\n        const foreColorInput = this.toolbar.querySelector('#foreColor input');\n        if (foreColorInput) {\n            foreColorInput.value = foreColor;\n        }\n\n        hiliteColor = hexFromColor(hiliteColor);\n        this.toolbar.style.setProperty('--hilite-color', hiliteColor);\n        const hiliteColorInput = this.toolbar.querySelector('#hiliteColor input');\n        if (hiliteColorInput) {\n            hiliteColorInput.value = hiliteColor.length <= 7 ? hiliteColor : hexFromColor(hiliteColor);\n        }\n    }\n\n    /**\n     * Applies the given command to the current selection. This does *NOT*:\n     * 1) update the history cursor\n     * 2) protect the unbreakables or unremovables\n     * 3) sanitize the result\n     * 4) create new history entry\n     * 5) follow the exact same operations that would be done following events\n     *    that would lead to that command\n     *\n     * For points 1 -> 4, @see _applyCommand\n     * For points 1 -> 5, @see execCommand\n     *\n     * @private\n     * @param {string} method\n     * @returns {?}\n     */\n    _applyRawCommand(method, ...args) {\n        const sel = this.document.getSelection();\n        if (sel.anchorNode && isProtected(sel.anchorNode)) {\n            return;\n        }\n        if (!(SELECTIONLESS_COMMANDS.includes(method) && args.length) &&\n            !this.isSelectionInEditable(sel) &&\n            !(closestElement(sel.anchorNode, \"*[t-field],*[t-out],*[t-esc]\") && FORMATTING_COMMANDS.includes(method))\n        ) {\n            // Do not apply commands out of the editable area.\n            return false;\n        }\n        if (!sel.isCollapsed && BACKSPACE_FIRST_COMMANDS.includes(method)) {\n            let range = getDeepRange(this.editable, {sel, splitText: true, select: true, correctTripleClick: true});\n            if (range &&\n                range.startContainer === range.endContainer &&\n                range.endContainer.nodeType === Node.TEXT_NODE &&\n                ZERO_WIDTH_CHARS.includes(range.cloneContents().textContent)\n            ) {\n                // We Collapse the selection and bypass deleteRange\n                // if the range content is only one ZWS.\n                sel.collapseToStart();\n                if (BACKSPACE_ONLY_COMMANDS.includes(method)) {\n                    this._applyRawCommand(method);\n                }\n                return;\n            }\n            this.deleteRange(sel);\n            if (BACKSPACE_ONLY_COMMANDS.includes(method)) {\n                return true;\n            }\n        }\n\n        this.options.beforeAnyCommand();\n\n        if (editorCommands[method]) {\n            return editorCommands[method](this, ...args);\n        }\n        if (method.startsWith('justify')) {\n            const mode = method.split('justify').join('').toLocaleLowerCase();\n            return this._align(mode === 'full' ? 'justify' : mode);\n        }\n        return sel.anchorNode[method](sel.anchorOffset, ...args);\n    }\n\n    /**\n     * Same as @see _applyRawCommand but adapt history, protects unbreakables\n     * and removables and sanitizes the result.\n     *\n     * @private\n     * @param {string} method\n     * @returns {?}\n     */\n    _applyCommand(...args) {\n        this._recordHistorySelection(true);\n        const result = this._protect(() => this._applyRawCommand(...args));\n        this.historyStep();\n        this._handleCommandHint();\n        return result;\n    }\n    /**\n     * @private\n     * @param {function} callback\n     * @param {number} [rollbackCounter]\n     * @returns {?}\n     */\n    _protect(callback, rollbackCounter) {\n        try {\n            const result = callback.call(this);\n            this.observerFlush();\n            if (this._toRollback) {\n                const torollbackCode = this._toRollback;\n                this.historyRollback(rollbackCounter);\n                return torollbackCode; // UNBREAKABLE_ROLLBACK_CODE || UNREMOVABLE_ROLLBACK_CODE\n            } else {\n                return result;\n            }\n        } catch (error) {\n            if (error === UNBREAKABLE_ROLLBACK_CODE || error === UNREMOVABLE_ROLLBACK_CODE) {\n                this.historyRollback(rollbackCounter);\n                return error;\n            } else {\n                throw error;\n            }\n        }\n    }\n    _activateContenteditable() {\n        this.observerUnactive('activateContenteditable');\n        this.editable.setAttribute('contenteditable', this.options.isRootEditable);\n\n        const editableAreas = this.options.getContentEditableAreas(this);\n        for (const node of editableAreas) {\n            if (!node.isContentEditable) {\n                if (isArtificialVoidElement(node) || node.nodeName === 'IMG') {\n                    node.classList.add('o_editable_media');\n                } else {\n                    node.setAttribute('contenteditable', true);\n                }\n            }\n        }\n        for (const node of this.options.getReadOnlyAreas()) {\n            node.setAttribute('contenteditable', false);\n        }\n        for (const element of this.options.getUnremovableElements()) {\n            element.classList.add(\"oe_unremovable\");\n        }\n        this.observerActive('activateContenteditable');\n    }\n\n    _stopContenteditable() {\n        this.observerUnactive('_stopContenteditable');\n        if (this.options.isRootEditable) {\n            this.editable.setAttribute('contenteditable', !this.options.isRootEditable);\n        }\n        for (const node of this.options.getContentEditableAreas(this)) {\n            if (node.getAttribute('contenteditable') === 'true') {\n                node.setAttribute('contenteditable', false);\n            }\n        }\n        this.observerActive('_stopContenteditable');\n    }\n\n    // TABLE MANAGEMENT\n    // ================\n\n    /**\n     * Handle the selection of table cells rectangularly (as opposed to line by\n     * line from left to right then top to bottom). If such a special selection\n     * was indeed applied, return true (and false otherwise).\n     *\n     * @private\n     * @param {MouseEvent|undefined} [ev]\n     * @returns {boolean}\n     */\n    _handleSelectionInTable(ev=undefined) {\n        const selection = this.document.getSelection();\n        // Selection could be gone if the document comes from an iframe that has been removed.\n        const anchorNode = selection && selection.rangeCount && selection.getRangeAt(0) && selection.anchorNode;\n        if (anchorNode && !ancestors(anchorNode).includes(this.editable)) {\n            return false;\n        }\n        const traversedNodes = getTraversedNodes(this.editable);\n        if (this._isResizingTable || !traversedNodes.some(node => !!closestElement(node, 'td') && !isProtected(node))) {\n            return false;\n        }\n        let range;\n        if (this.isFirefox) {\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 address this, we reset the selection to the _latestComputedSelection, ensuring that\n                // even when multiple ranges are selected, the original selection remains accessible.\n                this.historyResetLatestComputedSelection(true);\n            } else if (\n                ev &&\n                closestElement(ev.target, 'table') === closestElement(selection.anchorNode, 'table') &&\n                closestElement(ev.target, 'td') !== closestElement(selection.focusNode, 'td')\n            ) {\n                // When we modify a multiple range selection to a single range selection,\n                // Firefox stops updating the selection automatically.\n                // As a result, we need to manually update the selection based on the current target.\n                setSelection(selection.anchorNode, selection.anchorOffset, ev.target, 0);\n            }\n            // We need the triple click correction only for a bug in firefox\n            // where it gives a selection of a full cell as tr 0 tr 1. The\n            // correction makes it so it gives us the cell and not its neighbor.\n            // In all other cases we don't want to make that correction so as to\n            // avoid flicker when hovering borders.\n            range = getDeepRange(this.editable, { correctTripleClick: anchorNode && anchorNode.nodeName === 'TR' });\n        } else {\n            range = getDeepRange(this.editable);\n        }\n        const startTd = closestElement(range.startContainer, 'td');\n        const endTd = closestElement(range.endContainer, 'td');\n        let appliedCustomSelection = false;\n        // Get the top table ancestors at range bounds.\n        const startTable = ancestors(range.startContainer, this.editable).filter(node => node.nodeName === 'TABLE').pop();\n        const endTable = ancestors(range.endContainer, this.editable).filter(node => node.nodeName === 'TABLE').pop();\n        if (startTd !== endTd && startTable === endTable) {\n            if (!isProtected(startTable)) {\n                // The selection goes through at least two different cells ->\n                // select cells.\n                this._selectTableCells(range);\n                appliedCustomSelection = true;\n            }\n        } else if (!traversedNodes.every(node => node.parentElement && closestElement(node.parentElement, 'table')) && !selection.isCollapsed) {\n            // The selection goes through a table but also outside of it ->\n            // select the whole table.\n            this.observerUnactive('handleSelectionInTable');\n            const traversedTables = new Set(\n                traversedNodes\n                    .map((node) => closestElement(node, \"table\"))\n                    .filter((node) => !isProtected(node))\n            );\n            for (const table of traversedTables) {\n                // Don't apply several nested levels of selection.\n                if (table && !ancestors(table, this.editable).some(node => [...traversedTables].includes(node))) {\n                    table.classList.toggle('o_selected_table', true);\n                    for (const td of [...table.querySelectorAll('td')].filter(td => closestElement(td, 'table') === table)) {\n                        td.classList.toggle('o_selected_td', true);\n                    }\n                    appliedCustomSelection = true;\n                }\n            }\n            this.observerActive('handleSelectionInTable');\n        } else if (ev && startTd && !isProtected(startTd)) {\n            // We're redirected from a mousemove event.\n            const selectedNodes = getSelectedNodes(this.editable);\n            const cellContents = descendants(startTd);\n            const areCellContentsFullySelected = cellContents.filter(d => !isBlock(d)).every(child => selectedNodes.includes(child));\n            if (areCellContentsFullySelected) {\n                const SENSITIVITY = 5;\n                const rangeRect = range.getBoundingClientRect();\n                const isMovingAwayFromSelection = 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(range);\n                    appliedCustomSelection = true;\n                }\n            } else if (cellContents.filter(isBlock).every(isEmptyBlock) &&\n                Math.abs(ev.clientX - (this._lastMouseClickPosition ? this._lastMouseClickPosition[0] : ev.clientX)) >= 15\n            ) {\n                // Handle selecting an empty cell.\n                this._selectTableCells(range);\n                appliedCustomSelection = true;\n            }\n        }\n        return appliedCustomSelection;\n    }\n    /**\n     * Helper function to `_handleSelectionInTable`. Do the actual selection of\n     * cells in a table based on the current range.\n     *\n     * @private\n     * @see _handleSelectionInTable\n     * @param {Range} range\n     */\n    _selectTableCells(range) {\n        const table = closestElement(range.commonAncestorContainer, 'table');\n        if (!table) {\n            return;\n        }\n        this.observerUnactive('_selectTableCells');\n        const alreadyHadSelection = table.classList.contains('o_selected_table');\n        this.deselectTable(); // Undo previous selection.\n        table.classList.toggle('o_selected_table', true);\n        const columns = [...table.querySelectorAll('td')].filter(td => closestElement(td, 'table') === table);\n        const startCol = [range.startContainer, ...ancestors(range.startContainer, this.editable)]\n            .find(node => node.nodeName === 'TD' && closestElement(node, 'table') === table) || columns[0];\n        const endCol = [range.endContainer, ...ancestors(range.endContainer, this.editable)]\n            .find(node => node.nodeName === 'TD' && closestElement(node, 'table') === table) || 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] = [Math.min(startRowIndex, endRowIndex), Math.max(startRowIndex, endRowIndex)];\n        const [minColIndex, maxColIndex]  = [Math.min(startColIndex, endColIndex), Math.max(startColIndex, endColIndex)];\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((_, index) => index >= minColIndex && index <= maxColIndex)) {\n                td.classList.toggle('o_selected_td', true);\n            }\n        }\n        if (!alreadyHadSelection) {\n            this.toolbarShow();\n        }\n        this.observerActive('_selectTableCells');\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 {boolean}\n     */\n    _isHoveringTdBorder(ev) {\n        if (ev.target && ev.target.nodeName === 'TD' && ev.target.isContentEditable) {\n            const SENSITIVITY = 5;\n            const targetRect = ev.target.getBoundingClientRect();\n            if (ev.clientX <= targetRect.x + SENSITIVITY) {\n                return 'left';\n            } else if (ev.clientY <= targetRect.y + SENSITIVITY) {\n                return 'top';\n            } else if (ev.clientX >= targetRect.x + ev.target.clientWidth - SENSITIVITY) {\n                return 'right';\n            } else if (ev.clientY >= targetRect.y + ev.target.clientHeight - 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    _toggleTableResizeCursor(direction) {\n        this.editable.classList.remove('o_col_resize', 'o_row_resize');\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     * 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     * @private\n     * @param {MouseEvent} ev\n     */\n    _resizeTable(ev, direction, target1, target2) {\n        ev.preventDefault();\n        let position = target1 ? (target2 ? 'middle' : 'last') : 'first';\n        let [item, neighbor] = [target1 || target2, target2];\n        const table = closestElement(item, 'table');\n        const [sizeProp, positionProp, clientPositionProp] = direction === 'col' ? ['width', 'x', 'clientX'] : ['height', 'y', 'clientY'];\n\n        const isRTL = this.options.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 = `${direction === 'col' ? 'td' : 'tr'}: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 (td !== item && td !== neighbor && closestElement(td, 'table') === table && getColumnIndex(td) !== 0) {\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 = direction === 'col' ? (isRTL ? 'marginRight' : 'marginLeft') : 'marginTop';\n                const itemRect = item.getBoundingClientRect();\n                const tableStyle = getComputedStyle(table);\n                const currentMargin = pxToFloat(tableStyle[marginProp]);\n                let sizeDelta = itemRect[positionProp] - ev[clientPositionProp];\n                if (direction === 'col' && isRTL) {\n                    sizeDelta = 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 (direction === 'col' &&\n                        (hostCell && tableRect[endProp] + sizeDelta > hostCell.getBoundingClientRect()[endProp] - 5 ||\n                        childTable && childTable.getBoundingClientRect()[endProp] > itemRect[endProp] + sizeDelta - 5)) {\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] = [item.getBoundingClientRect(), neighbor.getBoundingClientRect()];\n                const [currentSize, newSize] = [itemRect[sizeProp], ev[clientPositionProp] - itemRect[positionProp]];\n                const editableStyle = getComputedStyle(this.editable);\n                const sizeDelta = newSize - currentSize;\n                const currentNeighborSize = neighborRect[sizeProp];\n                const newNeighborSize = currentNeighborSize - sizeDelta;\n                const maxWidth = this.editable.clientWidth - pxToFloat(editableStyle.paddingLeft) - pxToFloat(editableStyle.paddingRight);\n                const tableRect = table.getBoundingClientRect();\n                if (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 (direction === 'col' &&\n                        childTable && childTable.getBoundingClientRect().right > itemRect.right + sizeDelta - 5) {\n                        break\n                    }\n                    item.style[sizeProp] = newSize + 'px';\n                    if (direction === 'col') {\n                        neighbor.style[sizeProp] = (newNeighborSize > MIN_SIZE ? newNeighborSize : currentNeighborSize) + '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 = 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 (direction === 'col' &&\n                        (hostCell && tableRect[endProp] + sizeDelta > hostCell.getBoundingClientRect()[endProp] - 5 ||\n                        childTable && childTable.getBoundingClientRect()[endProp] > itemRect[endProp] + sizeDelta - 5)) {\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     * Show/hide and position the table row/column manipulation UI.\n     *\n     * @private\n     * @param {HTMLTableRowElement} [row=false]\n     * @param {HTMLTableCellElement} [column=false]\n     */\n    _toggleTableUi(row=false, column=false) {\n        if (row) {\n            this._rowUi.style.visibility = 'visible';\n            this._rowUiTarget = row;\n            this._positionTableUi(row);\n        } else {\n            this._rowUi.style.visibility = 'hidden';\n        }\n        if (column) {\n            this._columnUi.style.visibility = 'visible';\n            this._columnUiTarget = column;\n            this._positionTableUi(column);\n        } else {\n            this._columnUi.style.visibility = 'hidden';\n        }\n        if (row || column) {\n            this._tableUiTarget = closestElement(row || column, 'table');\n            this._tableUiTarget && this._tableUiTarget.addEventListener('mouseleave', () => this._toggleTableUi(), { once: true });\n        }\n    }\n    /**\n     * Position the table row/column tools (depending on whether a row or a cell\n     * is passed as argument).\n     *\n     * @private\n     * @param {HTMLTableRowElement|HTMLTableCellElement} element\n     */\n    _positionTableUi(element) {\n        if (!element.isConnected) {\n            return;\n        }\n        const tableUiContainerRect = this._tableUiContainer.getBoundingClientRect();\n        const isRtl = this.options.direction === 'rtl';\n        const isRow = element.nodeName === 'TR';\n        const ui = isRow ? this._rowUi : this._columnUi;\n        const elementRect = element.getBoundingClientRect();\n        const wrappedUi = ui.firstElementChild;\n        const table = closestElement(element, 'table');\n        const tableRect = table && table.getBoundingClientRect();\n        const resetTableSize = ui.querySelector('.o_reset_table_size');\n        if (table && !table.hasAttribute('style')) {\n            resetTableSize.classList.add('d-none');\n        } else {\n            resetTableSize.classList.remove('d-none');\n        }\n\n        let left;\n        let top;\n        if (isRow) {\n            if (isRtl) {\n                left = tableRect.right - tableUiContainerRect.x;\n            } else {\n                left = elementRect.left - tableUiContainerRect.left - wrappedUi.clientWidth;\n            }\n        } else if (isRtl) {\n            left = elementRect.left - tableUiContainerRect.left + wrappedUi.clientWidth;\n        } else {\n            left = elementRect.left - tableUiContainerRect.left - (isRow ? wrappedUi.clientWidth : 0);\n        }\n        top = elementRect.top - tableUiContainerRect.top - (isRow ? 0 : wrappedUi.clientHeight);\n\n        ui.style.left = left + 'px';\n        ui.style.top = top + 'px';\n        wrappedUi.style[isRow ? 'height' : 'width'] = elementRect[isRow ? 'height' : 'width'] + 'px';\n\n    }\n\n    // HISTORY\n    // =======\n\n    /**\n     * @private\n     * @returns {Object}\n     */\n    _computeHistorySelection() {\n        const sel = this.document.getSelection();\n        if (!(sel && sel.anchorNode)) {\n            return this._latestComputedSelection;\n        }\n        this._latestComputedSelection = {\n            anchorNode: sel.anchorNode,\n            anchorOffset: sel.anchorOffset,\n            focusNode: sel.focusNode,\n            focusOffset: sel.focusOffset,\n        };\n        if (this.isSelectionInEditable(sel)) {\n            this._latestComputedSelectionInEditable = this._latestComputedSelection;\n        }\n        return this._latestComputedSelection;\n    }\n    /**\n     * @private\n     * @param {boolean} [useCache=false]\n     */\n    _recordHistorySelection(useCache = false) {\n        this._currentStep.selection =\n            serializeSelection(\n                useCache ? this._latestComputedSelection : this._computeHistorySelection(),\n            ) || {};\n    }\n    /**\n     * Return true if the latest computed selection was inside an empty inline tag\n     *\n     * @private\n     * @return {boolean}\n     */\n    _isLatestComputedSelectionInsideEmptyInlineTag() {\n        if (!this._latestComputedSelection) {\n            return false;\n        }\n        const anchorNode = this._latestComputedSelection.anchorNode;\n        const focusNode = this._latestComputedSelection.focusNode;\n        const parentTextContent = anchorNode.parentElement? anchorNode.parentElement.textContent : null;\n        return anchorNode === focusNode && (['', ...ZERO_WIDTH_CHARS].includes(parentTextContent))\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._historySteps.length - 1; index >= 0; index--) {\n            if (\n                this._historySteps[index] &&\n                this._historySteps[index].clientId === this._collabClientId\n            ) {\n                const state = this._historyStepsStates.get(this._historySteps[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     * 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._historySteps.length - 1; index >= 0; index--) {\n            if (\n                this._historySteps[index] &&\n                this._historySteps[index].clientId === this._collabClientId\n            ) {\n                const state = this._historyStepsStates.get(this._historySteps[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    historyRevertUntil (toStepIndex) {\n        const lastStep = this._currentStep;\n        this.historyRevert(lastStep);\n        let stepIndex = this._historySteps.length - 1;\n        while (stepIndex > toStepIndex) {\n            const step = this._historySteps[stepIndex];\n            const stepState = this._historyStepsStates.get(step.id);\n            if (step.clientId === this._collabClientId && stepState !== 'consumed') {\n                this.historyRevert(this._historySteps[stepIndex]);\n                this._historyStepsStates.set(''+step.id, 'consumed');\n            }\n            stepIndex--;\n        }\n    }\n\n    // TOOLBAR\n    // =======\n\n    toolbarHide() {\n        this._updateToolbar(false);\n    }\n    toolbarShow() {\n        this._updateToolbar(true);\n    }\n    /**\n     * @private\n     * @param {boolean} [show]\n     */\n    _updateToolbar(show) {\n        if (!this.toolbar) {\n            return;\n        }\n        if (!this.autohideToolbar && this.toolbar.style.visibility !== 'visible') {\n            this.toolbar.style.visibility = 'visible';\n        }\n\n        const sel = this.document.getSelection();\n        if (!hasTableSelection(this.editable)) {\n            if (this.editable.classList.contains('o_col_resize') || this.editable.classList.contains('o_row_resize')) {\n                show = false;\n            }\n            if (!sel.anchorNode) {\n                show = false;\n            } else {\n                const selAncestors = [sel.anchorNode, ...ancestors(sel.anchorNode, this.editable)];\n                const isInStars = selAncestors.some(node => node.classList && node.classList.contains('o_stars'));\n                if (isInStars) {\n                    show = false;\n                }\n            }\n        }\n        if (this.autohideToolbar && !this.toolbar.contains(sel.anchorNode)) {\n            if (!this.isMobile) {\n                if (this.powerboxTablePicker.el.style.display === 'block') {\n                    this.toolbar.style.visibility = 'hidden';\n                    return;\n                }\n                if (show !== undefined) {\n                    this.toolbar.style.visibility = show ? 'visible' : 'hidden';\n                }\n                if (show === false) {\n                    for (const menu of this.toolbar.querySelectorAll('.dropdown-menu.show')) {\n                        menu.parentElement?.querySelector('[data-bs-toggle=\"dropdown\"]')?.click();\n                    };\n                    return;\n                }\n            }\n        }\n        const unlinkButton = this.toolbar.querySelector('#unlink');\n        if (!this.isSelectionInEditable(sel)) {\n            unlinkButton?.classList.add('d-none');\n            return;\n        }\n        const paragraphDropdownButton = this.toolbar.querySelector('#paragraphDropdownButton');\n        if (paragraphDropdownButton) {\n            for (const commandState of [\n                'justifyLeft',\n                'justifyRight',\n                'justifyCenter',\n                'justifyFull',\n            ]) {\n                const button = this.toolbar.querySelector('#' + commandState);\n                const direction = commandState === 'justifyFull'\n                    ? 'justify' : commandState.replace('justify', '').toLowerCase();\n                let isStateTrue = false;\n                const link = sel.anchorNode && closestElement(sel.anchorNode, 'a');\n                const linkBlock = link && closestBlock(link);\n                if (linkBlock) {\n                    // We don't support links with a width that is larger than\n                    // their contents so an alignment within the link is not\n                    // visible. Since the editor applies alignments to a node's\n                    // closest block, we show the alignment of the link's\n                    // closest block.\n                    const alignment = getComputedStyle(linkBlock).textAlign;\n                    isStateTrue = alignment === direction;\n                } else {\n                    isStateTrue = this.document.queryCommandState(commandState)\n                }\n                button.classList.toggle('active', isStateTrue);\n                const newClass = `fa-align-${direction}`;\n                paragraphDropdownButton.classList.toggle(newClass, isStateTrue);\n            }\n        }\n        if (sel.rangeCount) {\n            // queryCommandState does not take stylesheets into account\n            for (const format of ['bold', 'italic', 'underline', 'strikeThrough', 'switchDirection']) {\n                const formatButton = this.toolbar.querySelector(`#${format.toLowerCase()}`);\n                if (formatButton) {\n                    formatButton.classList.toggle('active', isSelectionFormat(this.editable, format));\n                }\n            }\n\n            const fontSizeEl = this.toolbar.querySelector(\"#fontSizeCurrentValue\");\n            if (fontSizeEl) {\n                fontSizeEl.value = Math.round(getFontSizeDisplayValue(sel,\n                    this.options.getCSSVariableValue,\n                    this.options.convertNumericToUnit\n                ));\n            }\n\n            const table = getInSelection(this.document, 'table');\n            const toolbarButton = this.toolbar.querySelector('.toolbar-edit-table');\n            if (toolbarButton) {\n                this.toolbar.querySelector('.toolbar-edit-table').style.display = table\n                    ? 'block'\n                    : 'none';\n            }\n\n            const selectionText = sel.toString().replace(/\\s+/g, \"\");\n            const translateDropdown = this.toolbar.querySelector('#translate');\n            if (translateDropdown) {\n                const translateDropdownBtn = translateDropdown.querySelector('.btn');\n                if (sel.isCollapsed) {\n                    translateDropdown.style.display = 'none';\n                } else {\n                    translateDropdown.style.display = '';\n                    translateDropdownBtn.classList[!selectionText ? 'add' : 'remove']('disabled');\n                }\n            }\n\n            const chatGptBtn = this.toolbar.querySelector('#open-chatgpt.btn');\n            if (chatGptBtn && !sel.isCollapsed) {\n                chatGptBtn.classList[!selectionText ? 'add' : 'remove']('disabled');\n            }\n        }\n        this.updateColorpickerLabels();\n        const listUIClasses = {UL: 'fa-list-ul', OL: 'fa-list-ol', CL: 'fa-tasks'};\n        const block = closestBlock(sel.anchorNode);\n        let activeLabel = undefined;\n        for (const [style, cssSelector, isList] of [\n            // TODO we might want to review this list to not mention o_xxx\n            // classes but be a setting instead? Probably after current\n            // refactorings being made in master.\n            ['paragraph', 'p:not(.small, .lead, .o_small)', false],\n            ['pre', 'pre', false],\n            ['heading1', 'h1:not(.display-1, .display-2, .display-3, .display-4)', false],\n            ['heading2', 'h2', false],\n            ['heading3', 'h3', false],\n            ['heading4', 'h4', false],\n            ['heading5', 'h5', false],\n            ['heading6', 'h6', false],\n            ['display-1', 'h1.display-1', false],\n            ['display-2', 'h1.display-2', false],\n            ['display-3', 'h1.display-3', false],\n            ['display-4', 'h1.display-4', false],\n            ['blockquote', 'blockquote', false],\n            // Note: this button will apply the \"o_small\" class but as an\n            // approximation, we display \"Small\" if this actually use the\n            // Bootstrap \"small\" class.\n            ['small', '.small, .o_small', false],\n            ['light', '.lead', false],\n            ['unordered', 'UL', true],\n            ['ordered', 'OL', true],\n            ['checklist', 'CL', true],\n        ]) {\n            const button = this.toolbar.querySelector('#' + style);\n            if (button && !block) {\n                button.classList.toggle('active', false);\n            } else if (button) {\n                const isActive = isList\n                    ? block.tagName === 'LI' && getListMode(block.parentElement) === cssSelector\n                    : block.matches(cssSelector);\n                button.classList.toggle('active', isActive);\n\n                if (!isList && isActive) {\n                    activeLabel = button.textContent;\n                }\n            }\n        }\n        if (block) {\n            const listMode = getListMode(block.parentElement);\n            const listDropdownButton = this.toolbar.querySelector('#listDropdownButton');\n            if (listDropdownButton) {\n                if (listMode) {\n                    listDropdownButton.classList.remove('fa-list-ul', 'fa-list-ol', 'fa-tasks');\n                    listDropdownButton.classList.add(listUIClasses[listMode]);\n                }\n                listDropdownButton.closest('button').classList.toggle('active', block.tagName === 'LI');\n            }\n        }\n\n        const styleSection = this.toolbar.querySelector('#style');\n        if (styleSection) {\n            if (!activeLabel) {\n                // If no element from the text style dropdown was marked as active,\n                // mark the paragraph one as active and use its label.\n                const firstButtonEl = styleSection.querySelector('#paragraph');\n                firstButtonEl.classList.add('active');\n                activeLabel = firstButtonEl.textContent;\n            }\n            styleSection.querySelector('button span').textContent = activeLabel;\n        }\n\n        const isInMedia = this.toolbar.classList.contains('oe-media');\n        const linkNode = getInSelection(this.document, 'a');\n        const linkButton = this.toolbar.querySelector('#create-link');\n        linkButton && linkButton.classList.toggle('active', !!linkNode);\n        // Hide unlink button if no link in selection, always hide on media\n        // elements.\n        unlinkButton?.classList.toggle('d-none', isInMedia || !linkNode);\n        const undoButton = this.toolbar.querySelector('#undo');\n        undoButton && undoButton.classList.toggle('disabled', !this.historyCanUndo());\n        const redoButton = this.toolbar.querySelector('#redo');\n        redoButton && redoButton.classList.toggle('disabled', !this.historyCanRedo());\n\n        // Hide create-link button if selection spans several blocks, always\n        // hide on media elements.\n        const range = getDeepRange(this.editable, { sel, correctTripleClick: true });\n        const spansBlocks = [...range.commonAncestorContainer.childNodes].some(isBlock);\n        linkButton?.classList.toggle('d-none', spansBlocks || isInMedia);\n\n        // Hide link button group if it has no visible button.\n        const linkBtnGroup = this.toolbar.querySelector('#link.btn-group');\n        linkBtnGroup?.classList.toggle('d-none', !linkBtnGroup.querySelector('.btn:not(.d-none)'));\n        if (this.autohideToolbar && !this.isMobile && !this.toolbar.contains(sel.anchorNode)) {\n            this._positionToolbar();\n        }\n    }\n\n    updateToolbarPosition() {\n        if (\n            this.autohideToolbar &&\n            !this.isMobile &&\n            getComputedStyle(this.toolbar).visibility === 'visible'\n        ) {\n            this._positionToolbar();\n        }\n    }\n\n    _positionToolbar() {\n        const OFFSET = 10;\n        const BASELINE_MARGIN = 5;\n        let isBottom = false;\n        // Toolbar display must not be none in order to calculate width and height.\n        this.toolbar.classList.toggle('d-none', false);\n        this.toolbar.style.maxWidth = window.innerWidth - OFFSET * 2 + 'px';\n        const sel = this.document.getSelection();\n        const range = sel.getRangeAt(0);\n        const isSelForward =\n            sel.anchorNode === range.startContainer && sel.anchorOffset === range.startOffset;\n        const startRect = range.startContainer.getBoundingClientRect && range.startContainer.getBoundingClientRect();\n        const selRect = range.getBoundingClientRect();\n        // In some undetermined circumstance in chrome, the selection rect is\n        // wrongly defined and result with all the values for x, y, width, and\n        // height to be 0. In that case, use the rect of the startContainer if\n        // possible.\n        const isSelectionPotentiallyBugged = [selRect.x, selRect.y, selRect.width, selRect.height].every( x => x === 0 );\n        let correctedSelectionRect = isSelectionPotentiallyBugged && startRect ? startRect : selRect;\n        const selAncestors = [sel.anchorNode, ...ancestors(sel.anchorNode, this.editable)];\n        // If a table is selected, we want to position the toolbar in function\n        // of the table, rather than follow the DOM selection.\n        const selectedTable = selAncestors.find(node => node.classList && node.classList.contains('o_selected_table'));\n        if (selectedTable) {\n            correctedSelectionRect = selectedTable.getBoundingClientRect();\n        }\n        const toolbarWidth = this.toolbar.offsetWidth;\n        const toolbarHeight = this.toolbar.offsetHeight;\n        const editorRect = this.editable.getBoundingClientRect();\n        const parentContextRect = this.options.getContextFromParentRect();\n        const scrollContainerRect = this.options.getScrollContainerRect();\n        const editorTopPos = Math.max(0, editorRect.top);\n        const scrollX = document.defaultView.scrollX;\n        const scrollY = document.defaultView.scrollY;\n        const rangeRects = [...range.getClientRects()];\n        // DOMRects on the same line might differ by a few pixels in their\n        // bottom value. We use BASELINE_MARGIN as threshold to differentiate\n        // between DOMRects on the same or different line.\n        const rangeSpansMultipleLines =\n            rangeRects.length > 1 && rangeRects.at(-1).bottom - rangeRects[0].bottom > BASELINE_MARGIN;\n\n        // Get left position.\n        let left = isSelForward || rangeSpansMultipleLines ?\n            correctedSelectionRect.left - OFFSET :\n            correctedSelectionRect.right + OFFSET - toolbarWidth;\n        // Ensure the toolbar doesn't overflow the editor on the left.\n        left = Math.max(OFFSET, left);\n        // Ensure the toolbar doesn't overflow the editor on the right.\n        left = Math.min(window.innerWidth - OFFSET - toolbarWidth, left);\n        // Offset left to compensate for parent context position (eg. Iframe).\n        const adjustedLeft = left + parentContextRect.left;\n        this.toolbar.style.left = scrollX + adjustedLeft + 'px';\n\n        // Get top position.\n        let top = correctedSelectionRect.top - toolbarHeight - OFFSET;\n        // Ensure the toolbar doesn't overflow the editor or scroll container on the top.\n        if (top < editorTopPos || top + parentContextRect.top - scrollContainerRect.top < OFFSET / 2) {\n            // Position the toolbar below the selection.\n            top = correctedSelectionRect.bottom + OFFSET;\n            isBottom = true;\n        }\n        // Offset top to compensate for parent context position (eg. Iframe).\n        top += parentContextRect.top;\n        this.toolbar.style.top = scrollY + top + 'px';\n\n        const hasArrow = !(rangeSpansMultipleLines || this.toolbar.classList.contains('oe-media'));\n        this.toolbar.classList.toggle('noarrow', !hasArrow);\n\n        let toolbarTop = top;\n        let toolbarBottom = top + toolbarHeight;\n        if (hasArrow) {\n            // Position the arrow.\n            let arrowLeftPos = (isSelForward && !isSelectionPotentiallyBugged ? correctedSelectionRect.right : correctedSelectionRect.left) - left - OFFSET;\n            // Ensure the arrow doesn't overflow the toolbar on the left.\n            arrowLeftPos = Math.max(OFFSET, arrowLeftPos);\n            // Ensure the arrow doesn't overflow the toolbar on the right.\n            arrowLeftPos = Math.min(toolbarWidth - OFFSET - 20, arrowLeftPos);\n            this.toolbar.style.setProperty('--arrow-left-pos', arrowLeftPos + 'px');\n            const arrowTopPos = isBottom ? -17 : toolbarHeight - 3;\n            this.toolbar.classList.toggle('toolbar-bottom', isBottom);\n            this.toolbar.style.setProperty('--arrow-top-pos', arrowTopPos + 'px');\n            // Calculate toolbar dimensions including the arrow.\n            toolbarTop = Math.min(top, top + arrowTopPos);\n            toolbarBottom = Math.max(toolbarBottom, top + arrowTopPos + 20);\n        }\n\n        // Hide toolbar if it overflows the scroll container.\n        const distToScrollContainer = Math.min(toolbarTop - scrollContainerRect.top,\n                                                scrollContainerRect.bottom - toolbarBottom);\n        const isToolbarOverflow = distToScrollContainer < OFFSET / 2;\n        if (isToolbarOverflow) {\n            this.toolbar.style.top = `${(Math.max(selRect.top, scrollContainerRect.top) + OFFSET)}px`\n        }\n    }\n\n    // PASTING / DROPPING\n\n    /**\n     * Prepare clipboard data (text/html) for safe pasting into the editor.\n     *\n     * @private\n     * @param {string} clipboardData\n     * @returns {Element}\n     */\n    _prepareClipboardData(clipboardData) {\n        const container = document.createElement('fake-container');\n        container.append(parseHTML(this.document, clipboardData));\n\n        for (const tableElement of container.querySelectorAll('table')) {\n            tableElement.classList.add('table', 'table-bordered', 'o_table');\n        }\n\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.match(`.${xlClass}[^\\{]*\\{(?<xlStyle>[^\\}]*)\\}`)\n                        .groups.xlStyle.replace('background:', 'background-color:');\n                    xlNode.setAttribute('style', xlNode.style.cssText + ';' + xlStyle)\n                }\n            }\n        }\n\n        for (const child of [...container.childNodes]) {\n            this._cleanForPaste(child);\n        }\n        // Force inline nodes at the root of the container into separate P\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        const fragment = document.createDocumentFragment();\n        let p = document.createElement('p');\n        for (const child of [...container.childNodes]) {\n            if (isBlock(child)) {\n                if (p.childNodes.length > 0) {\n                    fragment.appendChild(p);\n                    p = document.createElement('p');\n                }\n                fragment.appendChild(child);\n            } else {\n                p.appendChild(child);\n            }\n\n            if (p.childNodes.length > 0) {\n                fragment.appendChild(p);\n            }\n        }\n\n        // Split elements containing <br> into seperate elements for each line.\n        const brs = fragment.querySelectorAll('br');\n        for (const br of brs) {\n            const block = closestBlock(br);\n            if (\n                ['P', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(block.nodeName) &&\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 = splitAroundUntil(br, block);\n                // Remove the container unless it represented an empty line.\n                if (!isEmptyLine) {\n                    remainingBrContainer.remove();\n                }\n            }\n        }\n        return fragment;\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 childNodes;\n                if (node.nodeName === 'DIV' && [...node.childNodes].every(n => !isBlock(n))) {\n                    // Convert <div> to <p> to preserve the inline structure\n                    // while maintaining block-level behaviour.\n                    const dir = node.getAttribute('dir');\n                    const p = this.document.createElement('p');\n                    if (dir) {\n                        p.setAttribute('dir', dir);\n                    }\n                    p.append(...node.childNodes);\n                    node.replaceWith(p);\n                    childNodes = p.childNodes;\n                } else {\n                    // Unwrap the illegal node's contents.\n                    childNodes = unwrapContents(node);\n                }\n                for (const child of childNodes) {\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 (['S', 'U'].includes(node.nodeName) && node.childNodes.length === 1 && node.firstChild.nodeName === 'FONT') {\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(...fontNode.childNodes);\n                fontNode.appendChild(node);\n            } else if (node.nodeName === 'IMG' && node.getAttribute('aria-roledescription') === 'checkbox') {\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                if (CLIPBOARD_WHITELISTS.styledTags.includes(node.nodeName) && attribute.name === 'style') {\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            }\n            for (const klass of [...node.classList]) {\n                if (!this._isWhitelisted(klass)) {\n                    node.classList.remove(klass);\n                }\n            }\n            for (const child of [...node.childNodes]) {\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 && item.nodeType === Node.ATTRIBUTE_NODE) {\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 (\n                item.nodeType === Node.TEXT_NODE ||\n                (\n                    item.matches &&\n                    item.matches(CLIPBOARD_WHITELISTS.nodes)\n                )\n            );\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            node.nodeType !== Node.TEXT_NODE &&\n            node.matches([].concat(...Object.values(CLIPBOARD_BLACKLISTS)).join(','))\n        );\n    }\n    _safeSetAttribute(node, attributeName, attributeValue) {\n        const clone = document.createElement(node.tagName);\n        clone.setAttribute(attributeName, attributeValue);\n        DOMPurify.sanitize(clone, {\n            IN_PLACE: true,\n            ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n            ADD_ATTR: [\"contenteditable\"],\n        });\n        if (clone.hasAttribute(attributeName)) {\n            node.setAttribute(attributeName, clone.getAttribute(attributeName));\n        } else {\n            node.removeAttribute(attributeName);\n        }\n    }\n\n    disableAvatarForElement(element) {\n        this.enableAvatars();\n        for (const info of this._collabSelectionInfos.values()) {\n            if (info.avatarTargetElement === element) {\n                if (!info.avatarElement.classList.contains('opacity-0')) {\n                    info.avatarElement.classList.add('opacity-0');\n                }\n            }\n        }\n    }\n    enableAvatars() {\n        for (const element of this._avatarsContainer.querySelectorAll('.oe-collaboration-caret-avatar.opacity-0')) {\n            element.classList.remove('opacity-0');\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onBeforeInput(ev) {\n        this._lastBeforeInputType = ev.inputType;\n        // For chrome when we have this structure\n        // <div contenteditable=\"true\">\n        //     <ul>\n        //         <div contenteditable=\"false\">\n        //             <div contenteditable=\"true\">\n        //                 <p>\n        //                     text[]\n        //                 </p>\n        //             </div>\n        //         </div>\n        //     </ul>\n        // </div>\n        // clicking on `enter` doesn't works as expected and the `input` event is never\n        // triggered, to solve the problem we can use this hack where we stop the propagation\n        // and trigger manually the input event to simulate the correct flow.\n        if (ev.inputType ===\"insertParagraph\") {\n            const banner = closestElement(ev.target, \".o_editor_banner\");\n            if (banner && closestElement(banner, \"ul, ol\")) {\n                ev.preventDefault();\n                this._onInput(ev);\n                return;\n            }\n        }\n    }\n\n    /**\n     * If backspace/delete input, rollback the operation and handle the\n     * operation ourself. Needed for mobile, used for desktop for consistency.\n     *\n     * @private\n     */\n    _onInput(ev) {\n        // See if the Powerbox should be opened. If so, it will open at the end.\n        const newSelection = this.document.getSelection();\n        if (newSelection.anchorNode && isProtected(newSelection.anchorNode)) {\n            return;\n        }\n        const shouldOpenPowerbox = newSelection.isCollapsed && newSelection.rangeCount &&\n            ev.data === '/' && this.powerbox && !this.powerbox.isOpen &&\n            (!this.options.getPowerboxElement || !!this.options.getPowerboxElement());\n        if (shouldOpenPowerbox) {\n            // Undo input '/'.\n            this._powerboxBeforeStepIndex = this._historySteps.length - 1;\n        }\n        // Record the selection position that was computed on keydown or before\n        // contentEditable execCommand (whatever preceded the 'input' event)\n        this._recordHistorySelection(true);\n        const selection = this._currentStep.selection;\n        const { anchorNodeOid, anchorOffset, focusNodeOid, focusOffset } = selection || {};\n        const wasCollapsed =\n            !selection || (focusNodeOid === anchorNodeOid && focusOffset === anchorOffset);\n        // Sometimes google chrome wrongly triggers an input event with `data`\n        // being `null` on `deleteContentForward` `insertParagraph`. Luckily,\n        // chrome provide the proper signal with the event `beforeinput`.\n        const isChromeDeleteforward =\n            ev.inputType === 'insertText' &&\n            ev.data === null &&\n            this._lastBeforeInputType === 'deleteContentForward';\n        const isChromeInsertParagraph =\n            ev.inputType === 'insertText' &&\n            ev.data === null &&\n            this._lastBeforeInputType === 'insertParagraph';\n        const isCompositionEvent =\n            ev.inputType === \"insertCompositionText\" ||\n            (ev.inputType === \"insertText\" &&\n                (this.keyboardType === KEYBOARD_TYPES.VIRTUAL ||\n                    this.isMobile));\n        if (isCompositionEvent) {\n            this._fromCompositionText = true;\n        }\n        if (this.keyboardType === KEYBOARD_TYPES.PHYSICAL || !wasCollapsed) {\n            // Most deletion cases in complex HTML like Bootstrap etc can end\n            // with a wrong result if done by the contenteditable itself.\n            // Intervene as soon as the selection was not collapsed, except\n            // while composing. In that case the composition should be left\n            // alone unless the selection was spanning different blocks.\n            const anchorNode = this.idFind(anchorNodeOid);\n            const focusNode = this.idFind(focusNodeOid);\n            const wasSelectingAcrossDifferentBlocks =\n                anchorNode &&\n                focusNode &&\n                closestBlock(anchorNode) !== closestBlock(focusNode);\n            const shouldInterveneForDeletion =\n                !this._fromCompositionText ||\n                wasSelectingAcrossDifferentBlocks;\n            if (ev.inputType === 'deleteContentBackward' && shouldInterveneForDeletion) {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                this._applyCommand('oDeleteBackward');\n            } else if (\n                (ev.inputType === 'deleteContentForward' || isChromeDeleteforward) &&\n                shouldInterveneForDeletion\n            ) {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                this._applyCommand('oDeleteForward');\n            } else if (\n                (['insertParagraph', 'insertLineBreak'].includes(ev.inputType) || isChromeInsertParagraph)\n            ) {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                this._handleAutomaticLinkInsertion();\n                getDeepRange(this.editable, { select: true, correctTripleClick: true });\n                // To remove only the anchor cell's content when multiple table cells are selected on Enter,\n                // we need to change the selection to focus only on the anchor cell. This can't be done in `oEnter`\n                // because `deleteRange` responsible for removing content, execute before `oEnter` in `_applyRawCommand`.\n                // Therefore, the anchor cell selection should be adjusted before `_applyRawCommand` is called.\n                const anchorTD = closestElement(newSelection.anchorNode, '.o_selected_td');\n                const focusTD = closestElement(newSelection.focusNode, '.o_selected_td');\n                if (anchorTD && focusTD && closestElement(anchorTD, 'table') === closestElement(focusTD, 'table')) {\n                    this.deselectTable();\n                    setSelection(anchorTD.firstChild, 0, anchorTD.lastChild, nodeSize(anchorTD.lastChild));\n                }\n                if (ev.inputType === 'insertLineBreak' || this._applyCommand('oEnter') === UNBREAKABLE_ROLLBACK_CODE) {\n                    this._applyCommand('oShiftEnter');\n                }\n            } else if (['insertText', 'insertCompositionText'].includes(ev.inputType)) {\n                const selection = this.document.getSelection();\n                // Unit tests events are not trusted by the browser,\n                // the insertText has to be done manualy.\n                const isUnitTests = !ev.isTrusted && this.testMode;\n                // we cannot trust the browser to keep the selection inside empty tags.\n                const latestSelectionInsideEmptyTag = this._isLatestComputedSelectionInsideEmptyInlineTag();\n                const shouldInterveneForInsertion = !wasCollapsed && shouldInterveneForDeletion;\n                if (\n                    shouldInterveneForInsertion ||\n                    latestSelectionInsideEmptyTag ||\n                    isUnitTests\n                ) {\n                    ev.preventDefault();\n                    if (!isUnitTests) {\n                        // First we need to undo the character inserted by the browser.\n                        // Since the unit test Event is not trusted by the browser, we don't\n                        // need to undo the char during the unit tests.\n                        // @see https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted\n                        this._protect(() => this._applyRawCommand('oDeleteBackward'));\n                    }\n                    if (latestSelectionInsideEmptyTag) {\n                        // Restore the selection inside the empty Element.\n                        const selectionBackup = this._latestComputedSelection;\n                        setSelection(selectionBackup.anchorNode, selectionBackup.anchorOffset);\n                    }\n                    // When the spellcheck of Safari modify text, ev.data is\n                    // null and the string can be found within ev.dataTranser.\n                    insertText(selection, ev.data === null ? ev.dataTransfer.getData('text/plain') : ev.data);\n                    selection.collapseToEnd();\n                }\n                const blockEl = closestBlock(selection.anchorNode);\n                const stringToConvert = blockEl.textContent.substring(0, selection.anchorOffset);\n                const shouldCreateNumberList = (/^(?:[1aA])[.)]\\s$/).test(stringToConvert);\n                const shouldCreateBulletList = (/^[-*]\\s$/).test(stringToConvert);\n                if (ev.data === '`' && !closestElement(selection.anchorNode, 'code')) {\n                    // We just inserted a backtick, check if there was another\n                    // one in the text.\n                    const range = getDeepRange(this.editable);\n                    let textNode = range.startContainer;\n                    let offset = range.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                    setSelection(textNode, 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.historyStep();\n                        const insertedBacktickIndex = offset - 1;\n                        const textBeforeInsertedBacktick = textNode.textContent.substring(0, insertedBacktickIndex - 1);\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(1, textNode.textContent.length - 1);\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 (!codeElement.previousSibling || codeElement.previousSibling.nodeType !== Node.TEXT_NODE) {\n                            codeElement.before(document.createTextNode('\\u200B'));\n                        }\n                        if (isClosingForward) {\n                            // Move selection out of code element.\n                            codeElement.after(document.createTextNode('\\u200B'));\n                            setSelection(codeElement.nextSibling, 1);\n                        } else {\n                            setSelection(codeElement.firstChild, 0);\n                        }\n                    }\n                } else if ((shouldCreateNumberList || shouldCreateBulletList) &&\n                    !closestElement(selection.anchorNode, 'li')\n                ) {\n                    this.historyStep();\n                    const range = selection.getRangeAt(0);\n                    range.setStartBefore(blockEl.firstChild);\n                    range.extractContents();\n                    fillEmpty(blockEl);\n                    this.historyPauseSteps();\n                    if (shouldCreateNumberList) {\n                        this._applyCommand('toggleList', 'OL');\n                        // When the anchorNode is a context block and a list is\n                        // being created inside it, ensure to navigate to the\n                        // deepest node.\n                        const [deepsetNode] = getDeepestPosition(selection.anchorNode, selection.anchorOffset);\n                        const closestOl = closestElement(deepsetNode, 'OL');\n                        if (stringToConvert.startsWith('A')) {\n                            closestOl.style.listStyle = 'upper-alpha';\n                        } else if (stringToConvert.startsWith('a')) {\n                            closestOl.style.listStyle = 'lower-alpha';\n                        }\n                    } else if (shouldCreateBulletList) {\n                        this._applyCommand('toggleList', 'UL');\n                    }\n                    this.historyUnpauseSteps();\n                }\n                this.historyStep();\n            } else {\n                this.historyStep();\n            }\n        }\n        if (!isCompositionEvent) {\n            this._fromCompositionText = false;\n        }\n        if (shouldOpenPowerbox) {\n            this._isPowerboxOpenOnInput = true;\n            this.powerbox.open();\n        }\n    }\n\n    _onClipboardCut(clipboardEvent) {\n        this._onClipboardCopy(clipboardEvent);\n        this._recordHistorySelection();\n        this.deleteRange();\n        this.historyStep();\n    }\n    _onClipboardCopy(clipboardEvent) {\n        if (!this.isSelectionInEditable()) {\n            return;\n        }\n        clipboardEvent.preventDefault();\n        const selection = this.document.getSelection();\n        const range = selection.getRangeAt(0);\n        let rangeContent = range.cloneContents();\n        if (!rangeContent.hasChildNodes()) {\n            return;\n        }\n        // Repair the copied range.\n        if (rangeContent.firstChild.nodeName === 'LI') {\n            const list = range.commonAncestorContainer.cloneNode();\n            list.replaceChildren(...rangeContent.childNodes);\n            rangeContent = list;\n        }\n        if (rangeContent.firstChild.nodeName === 'TR' || rangeContent.firstChild.nodeName === 'TD') {\n            // We enter this case only if selection is within single table.\n            const table = closestElement(range.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 && !!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) { // ignore nested\n                        td.remove();\n                    }\n                }\n                const trsWithoutTd = Array.from(tableClone.querySelectorAll('tr')).filter(row => !row.querySelector('td'));\n                for (const tr of trsWithoutTd) {\n                    if (closestElement(tr, 'table') === tableClone) { // 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            rangeContent = tableClone;\n        }\n        const startTable = closestElement(range.startContainer, 'table');\n        if (rangeContent.firstChild.nodeName === 'TABLE' && startTable) {\n            // Make sure the full leading table is copied.\n            rangeContent.firstChild.after(startTable.cloneNode(true));\n            rangeContent.firstChild.remove();\n        }\n        const endTable = closestElement(range.endContainer, 'table');\n        if (rangeContent.lastChild.nodeName === 'TABLE' && endTable) {\n            // Make sure the full trailing table is copied.\n            rangeContent.lastChild.before(endTable.cloneNode(true));\n            rangeContent.lastChild.remove();\n        }\n\n        const commonAncestorElement = closestElement(range.commonAncestorContainer);\n        if (commonAncestorElement && !isBlock(rangeContent.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 = [commonAncestorElement, ...ancestors(commonAncestorElement, blockEl)];\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) || paragraphRelatedElements.includes(ancestor.nodeName)) {\n                    const clone = ancestor.cloneNode();\n                    clone.append(...rangeContent.childNodes);\n                    rangeContent.appendChild(clone);\n                }\n            }\n        }\n        const dataHtmlElement = document.createElement('data');\n        dataHtmlElement.append(rangeContent);\n        const odooHtml = dataHtmlElement.innerHTML.replace(/\\uFEFF/g, \"\");\n        const odooText = selection.toString().replace(/\\uFEFF/g, \"\");\n        clipboardEvent.clipboardData.setData('text/plain', odooText);\n        clipboardEvent.clipboardData.setData('text/html', odooHtml);\n        clipboardEvent.clipboardData.setData('text/odoo-editor', odooHtml);\n    }\n    /**\n     * @private\n     */\n    _onKeyDown(ev) {\n        const selection = this.document.getSelection();\n        if (selection.anchorNode && isProtected(selection.anchorNode)) {\n            return;\n        }\n        if (this.document.querySelector(\".transfo-container\")){\n            ev.preventDefault();\n            return;\n        }\n        this.keyboardType =\n            ev.key === 'Unidentified' ? KEYBOARD_TYPES.VIRTUAL : KEYBOARD_TYPES.PHYSICAL;\n        this._currentKeyPress = ev.key;\n        // If the pressed key has a printed representation, the returned value\n        // is a non-empty Unicode character string containing the printable\n        // representation of the key. In this case, call `deleteRange` before\n        // inserting the printed representation of the character.\n        if (/^.$/u.test(ev.key) && !ev.ctrlKey && !ev.metaKey && (isMacOS() || !ev.altKey)) {\n            const selection = this.document.getSelection();\n            if (selection && !selection.isCollapsed && this.isSelectionInEditable(selection)) {\n                this.deleteRange(selection);\n            }\n        }\n        if (ev.key === 'Backspace') {\n            // backspace\n            const selection = this.document.getSelection();\n            if (!ev.ctrlKey && !ev.metaKey) {\n                if (selection.isCollapsed && !this._fromCompositionText) {\n                    // We need to hijack it because firefox doesn't trigger a\n                    // deleteBackward input event with a collapsed selection in\n                    // front of a contentEditable=\"false\" (eg: font awesome).\n                    ev.preventDefault();\n                    this._applyCommand('oDeleteBackward');\n                }\n            } else if (selection.isCollapsed && selection.anchorNode) {\n                const anchor = (selection.anchorNode.nodeType !== Node.TEXT_NODE && selection.anchorOffset) ?\n                    selection.anchorNode[selection.anchorOffset] : selection.anchorNode;\n                const element = closestBlock(anchor);\n                if (isEmptyBlock(element) && element.parentElement.children.length === 1) {\n                    // Prevent removing a <p> if it is the last element of its\n                    // parent.\n                    ev.preventDefault();\n                    if (element.tagName !== 'P') {\n                        // Replace an empty block which is not a <p> by a <p>\n                        const paragraph = this.document.createElement('P');\n                        const br = this.document.createElement('BR');\n                        paragraph.append(br);\n                        element.before(paragraph);\n                        const result = this._protect(() => element.remove());\n                        if (result !== UNBREAKABLE_ROLLBACK_CODE && result !== UNREMOVABLE_ROLLBACK_CODE) {\n                            setCursorStart(paragraph);\n                            this.historyStep();\n                        }\n                    }\n                }\n            }\n        } else if (ev.key === 'Tab') {\n            // Tab\n            const tabHtml = '<span class=\"oe-tabs\" contenteditable=\"false\">\\u0009</span>\\u200B';\n            const sel = this.document.getSelection();\n            const closestUnbreakable = closestElement(sel.anchorNode, isUnbreakable);\n            const closestTableOrLi = closestElement(sel.anchorNode, 'table, li');\n            const closestUnbreakableOrLi = closestElement(sel.anchorNode, [\"li\", closestUnbreakable.nodeName].join(\",\"));\n            if (closestTableOrLi && closestTableOrLi.nodeName === 'TABLE') {\n                this._onTabulationInTable(ev);\n            } else if (\n                !ev.shiftKey &&\n                sel.isCollapsed &&\n                closestUnbreakableOrLi.nodeName !== 'LI'\n            ) {\n                // Indent text (collapsed selection).\n                this.execCommand('insert', parseHTML(this.document, tabHtml));\n            } else {\n                // Indent/outdent selection.\n                // Split traversed nodes into list items and the rest.\n                const listItems = new Set();\n                const nonListItems = new Set();\n                for (const node of getTraversedNodes(this.editable)) {\n                    const closestLi = closestElement(node, 'li');\n                    const target = closestLi || node;\n                    if (!(target.querySelector && target.querySelector('li'))) {\n                        if (closestLi) {\n                            listItems.add(closestLi);\n                        } else {\n                            nonListItems.add(node);\n                        }\n                    }\n                }\n\n                const restore = preserveCursor(this.document);\n\n                // Indent/outdent list items.\n                for (const listItem of listItems) {\n                    if (ev.shiftKey) {\n                        listItem.oShiftTab(0);\n                    } else {\n                        listItem.oTab(0);\n                    }\n                }\n\n                // Indent/outdent the rest.\n                if (ev.shiftKey) {\n                    const editorTabs = new Set(\n                        [...nonListItems].map(node => {\n                            const block = closestBlock(node);\n                            return descendants(block).find(child => isEditorTab(child));\n                        }).filter(node => (\n                            // Filter out tabs preceded by visible text.\n                            node && !getAdjacentPreviousSiblings(node).some(sibling => (\n                                sibling.nodeType === Node.TEXT_NODE && !/^[\\u200B\\s]*$/.test(sibling.textContent)\n                            ))\n                    )));\n                    for (const tab of editorTabs) {\n                        let { anchorNode, anchorOffset, focusNode, focusOffset } = sel;\n                        const updateAnchor = anchorNode === tab.nextSibling;\n                        const updateFocus = focusNode === tab.nextSibling;\n                        let zwsRemoved = 0;\n                        while (tab.nextSibling && tab.nextSibling.nodeType === Node.TEXT_NODE && tab.nextSibling.textContent.startsWith('\\u200B')) {\n                            splitTextNode(tab.nextSibling, 1, DIRECTIONS.LEFT);\n                            tab.nextSibling.remove();\n                            zwsRemoved++;\n                        }\n                        if (updateAnchor || updateFocus) {\n                            setSelection(\n                                updateAnchor ? tab.nextSibling : anchorNode,\n                                updateAnchor ? Math.max(0, anchorOffset - zwsRemoved) : anchorOffset,\n                                updateFocus ? tab.nextSibling : focusNode,\n                                updateFocus ? Math.max(0, focusOffset - zwsRemoved) : focusOffset\n                            );\n                        }\n                        tab.remove();\n                    };\n                } else {\n                    const tab = parseHTML(this.document, tabHtml);\n                    for (const block of new Set([...nonListItems].map(node => closestBlock(node)).filter(node => node))) {\n                        block.prepend(tab.cloneNode(true));\n                    }\n                    restore();\n                }\n                this.historyStep();\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n        } else if (ev.key === ' ') {\n            this._handleAutomaticLinkInsertion();\n        } else if (IS_KEYBOARD_EVENT_UNDO(ev)) {\n            // Ctrl-Z\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.historyUndo();\n        } else if (IS_KEYBOARD_EVENT_REDO(ev)) {\n            // Ctrl-Y\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.historyRedo();\n        } else if (IS_KEYBOARD_EVENT_BOLD(ev)) {\n            // Ctrl-B\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('bold');\n            this.historyResetLatestComputedSelection(true);\n        } else if (IS_KEYBOARD_EVENT_ITALIC(ev)) {\n            // Ctrl-I\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('italic');\n            this.historyResetLatestComputedSelection(true);\n        } else if (IS_KEYBOARD_EVENT_UNDERLINE(ev)) {\n            // Ctrl-U\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('underline');\n            this.historyResetLatestComputedSelection(true);\n        } else if (IS_KEYBOARD_EVENT_STRIKETHROUGH(ev)) {\n            // Ctrl-5 / Ctrl-shift-(\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('strikeThrough');\n            this.historyResetLatestComputedSelection(true);\n        } else if (IS_KEYBOARD_EVENT_LEFT_ARROW(ev) || IS_KEYBOARD_EVENT_RIGHT_ARROW(ev)) {\n            const isRTL = this.options.direction === 'rtl';\n            const previousName = isRTL ? 'next' : 'previous';\n            const nextName = isRTL ? 'previous' : 'next';\n            const side = ev.key === 'ArrowLeft' ? previousName : nextName;\n            const selection = this.document.getSelection();\n            let { anchorNode, anchorOffset, focusNode, focusOffset } = selection || {};\n            if (ev.shiftKey) {\n                // Since selection can't traverse contenteditable=\"false\"\n                // elements, we adjust the selection to the sibling of\n                // non editable element.\n                const isFocusContentEditable = focusNode.isContentEditable;\n                if (focusNode.nodeType === Node.ELEMENT_NODE) {\n                    getDeepRange(this.editable, { selection, select: !isFocusContentEditable, correctTripleClick: !isFocusContentEditable });\n                }\n                ({ anchorNode, anchorOffset, focusNode, focusOffset } = selection)\n                const currentBlock = closestBlock(focusNode);\n                const isAtBoundary = side === 'previous'\n                    ? firstLeaf(currentBlock) === focusNode && focusOffset === 0\n                    : lastLeaf(currentBlock) === focusNode && focusOffset === nodeSize(focusNode);\n                const adjacentBlock = side === 'previous' ? currentBlock.previousElementSibling : currentBlock.nextElementSibling;\n                const targetBlock = side === 'previous' ? adjacentBlock?.previousElementSibling : adjacentBlock?.nextElementSibling;\n                if (!adjacentBlock?.isContentEditable && targetBlock && isAtBoundary) {\n                    const leafNode = lastLeaf(targetBlock);\n                    const offset = side === 'previous' ? nodeSize(leafNode) : 0;\n                    selection.extend(leafNode, offset);\n                    ev.preventDefault();\n                }\n            }\n            // If the selection is at the edge of a code element at the edge of\n            // its parent, make sure there's a zws next to it, where the\n            // selection can then be set.\n            const codeElement = anchorNode && closestElement(anchorNode, 'code');\n            const siblingProperty = `${side}Sibling`;\n            if (\n                codeElement?.classList.contains('o_inline_code') &&\n                (\n                    (side === 'previous' && !anchorOffset) ||\n                    (side === 'next' && anchorOffset === nodeSize(anchorNode))\n                ) &&\n                codeElement[siblingProperty]?.nodeType !== Node.TEXT_NODE &&\n                !isZWS(codeElement[siblingProperty])\n            ) {\n                codeElement[side === 'previous' ? 'before' : 'after'](document.createTextNode('\\u200B'));\n                setSelection(codeElement[siblingProperty], side === 'previous' ? 0 : 1);\n            } else {\n                // Move selection if adjacent character is zero-width space.\n                let didSkipFeff = false;\n                let adjacentCharacter = getAdjacentCharacter(this.editable, side);\n                let previousSelection; // Is used to stop if `modify` doesn't move the selection.\n                const hasSelectionChanged = (oldSelection = {}) => {\n                    const newSelection = this.document.getSelection();\n                    return (\n                        oldSelection.anchorNode !== newSelection.anchorNode ||\n                        oldSelection.anchorOffset !== newSelection.anchorOffset ||\n                        oldSelection.focusNode !== newSelection.focusNode ||\n                        oldSelection.focusOffset !== newSelection.focusOffset\n                    );\n                };\n                while (ZERO_WIDTH_CHARS.includes(adjacentCharacter) && hasSelectionChanged(previousSelection)) {\n                    const selection = this.document.getSelection();\n                    previousSelection = {...selection};\n                    selection.modify(\n                        ev.shiftKey ? 'extend' : 'move',\n                        side === 'previous' ? 'backward' : 'forward',\n                        'character',\n                    );\n                    didSkipFeff = didSkipFeff || adjacentCharacter === '\\ufeff';\n                    adjacentCharacter = getAdjacentCharacter(this.editable, side);\n                }\n                if (didSkipFeff && !ev.shiftKey) {\n                    // If moving, just skip the zws then stop. Otherwise, do as if\n                    // they weren't there.\n                    ev.preventDefault();\n                    ev.stopPropagation();\n                }\n            }\n        } else if ((IS_KEYBOARD_EVENT_UP_ARROW(ev) || IS_KEYBOARD_EVENT_DOWN_ARROW(ev)) && ev.shiftKey) {\n            // Since selection can't traverse contenteditable=\"false\" elements,\n            // we adjust the selection to the sibling of non editable element.\n            const selection = this.document.getSelection();\n            const isFocusContentEditable = selection.focusNode.isContentEditable;\n            if (selection.focusNode.nodeType === Node.ELEMENT_NODE) {\n                getDeepRange(this.editable, { selection, select: !isFocusContentEditable, correctTripleClick: !isFocusContentEditable });\n            }\n            const currentBlock = closestBlock(selection.focusNode);\n            const isAtBoundary = ev.key === 'ArrowUp'\n                ? firstLeaf(currentBlock) === selection.focusNode && selection.focusOffset === 0\n                : lastLeaf(currentBlock) === selection.focusNode && selection.focusOffset === nodeSize(selection.focusNode);\n            const adjacentBlock = ev.key === 'ArrowUp' ? currentBlock.previousElementSibling : currentBlock.nextElementSibling;\n            const targetBlock = ev.key === 'ArrowUp' ? adjacentBlock?.previousElementSibling : adjacentBlock?.nextElementSibling;\n            if (!adjacentBlock?.isContentEditable && targetBlock && isAtBoundary) {\n                const leafNode = lastLeaf(targetBlock);\n                const offset = ev.key === 'ArrowUp' ? nodeSize(leafNode) : 0;\n                selection.extend(leafNode, offset);\n                ev.preventDefault();\n            }\n        }\n    }\n    /**\n     * @private\n     */\n    _onSelectionChange() {\n        const currentKeyPress = this._currentKeyPress;\n        delete this._currentKeyPress;\n        const selection = this.document.getSelection();\n        if (!selection) {\n            // Because the `selectionchange` event is async, the selection can\n            // be null if the node has been removed between the moment the\n            // selection was moved and the moment the event is triggered.\n            return;\n        }\n        const anchorNode = selection.anchorNode;\n        // Correct cursor if at editable root.\n        if (\n            selection.isCollapsed &&\n            anchorNode === this.editable &&\n            !this.options.allowInlineAtRoot\n        ) {\n            this._fixSelectionOnEditableRoot(selection, currentKeyPress);\n            // The _onSelectionChange handler is going to be triggered again.\n            return;\n        }\n        let appliedCustomSelection = false;\n        if (selection.rangeCount && selection.getRangeAt(0)) {\n            appliedCustomSelection = this._handleSelectionInTable();\n            if (!appliedCustomSelection) {\n                this.deselectTable();\n            }\n        }\n        const isSelectionInEditable = this.isSelectionInEditable(selection);\n        if (!appliedCustomSelection) {\n            this._updateToolbar(!selection.isCollapsed && isSelectionInEditable);\n        }\n        if (!isSelectionInEditable) {\n            return;\n        }\n        // When CTRL+A in the editor, sometimes the browser use the editable\n        // element as an anchor & focus node. This is an issue for the commands\n        // and the toolbar so we need to fix the selection to be based on the\n        // editable children. Calling `getDeepRange` ensure the selection is\n        // limited to the editable.\n        if (\n            selection.anchorNode === this.editable &&\n            selection.focusNode === this.editable &&\n            selection.anchorOffset === 0 &&\n            selection.focusOffset === [...this.editable.childNodes].length\n        ) {\n            getDeepRange(this.editable, {select: true});\n            // The selection is changed in `getDeepRange` and will therefore\n            // re-trigger the _onSelectionChange.\n            return;\n        }\n        this._resetLinkInSelection();\n        // Compute the current selection on selectionchange but do not record it. Leave\n        // that to the command execution or the 'input' event handler.\n        this._computeHistorySelection();\n\n        if (this._currentMouseState === 'mouseup') {\n            this._fixFontAwesomeSelection();\n        }\n        if (\n            selection.rangeCount &&\n            selection.getRangeAt(0) &&\n            this.options.onCollaborativeSelectionChange\n        ) {\n            this.options.onCollaborativeSelectionChange(this.getCurrentCollaborativeSelection());\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    _resetLinkInSelection() {\n        const selection = this.document.getSelection();\n        const [anchorLink, focusLink] = [selection?.anchorNode, selection?.focusNode]\n            .map(node => closestElement(node, 'a:not(.btn)'));\n        const isSingleLinkInSelection =\n            anchorLink &&\n            anchorLink === focusLink &&\n            isLinkEligibleForZwnbsp(this.editable, anchorLink);\n        if (isSingleLinkInSelection) {\n            this.observerUnactive('add.o_link_in_selection');\n            anchorLink.classList.add('o_link_in_selection');\n            this.observerActive('add.o_link_in_selection');\n        }\n        for (const link of this.editable.querySelectorAll('.o_link_in_selection')) {\n            if (link !== anchorLink) {\n                this.observerUnactive('remove.o_link_in_selection');\n                link.classList.remove('o_link_in_selection');\n                this.observerActive('remove.o_link_in_selection');\n            }\n        };\n    }\n    /**\n     * Returns true if the current selection is inside the editable.\n     *\n     * @param {Object} [selection]\n     * @returns {boolean}\n     */\n    isSelectionInEditable(selection) {\n        selection = selection || this.document.getSelection();\n        if (selection && selection.anchorNode && selection.focusNode) {\n            const anchorElement = closestElement(selection.anchorNode);\n            const focusElement = closestElement(selection.focusNode);\n            return anchorElement && anchorElement.isContentEditable && focusElement && focusElement.isContentEditable &&\n                this.editable.contains(selection.anchorNode) && this.editable.contains(selection.focusNode);\n        } else {\n            return false;\n        }\n    }\n    /**\n     * Returns true if the current selection is in at least one block Element\n     * relative to the current contentEditable root.\n     *\n     * @returns {boolean}\n     */\n    isSelectionInBlockRoot() {\n        const selection = this.document.getSelection();\n        let selectionInBlockRoot;\n        let currentNode = closestElement(selection.anchorNode);\n        while (\n            !currentNode.classList.contains('o_editable') &&\n            !currentNode.classList.contains('odoo-editor-editable') &&\n            !selectionInBlockRoot\n            ) {\n            selectionInBlockRoot = isBlock(currentNode);\n            currentNode = currentNode.parentElement;\n        }\n        return !!selectionInBlockRoot;\n    }\n\n    /**\n     * @private\n     */\n    _compositionStep() {\n        if (this._fromCompositionText) {\n            this._fromCompositionText = false;\n            this.sanitize();\n            this.historyStep();\n        }\n    }\n\n    getCurrentCollaborativeSelection() {\n        const selection = this._latestComputedSelection || this._computeHistorySelection();\n        return {\n            selection: selection ? serializeSelection(selection) : {\n                anchorNodeOid: undefined,\n                anchorOffset: undefined,\n                focusNodeOid: undefined,\n                focusOffset: undefined,\n            },\n            color: this._collabSelectionColor,\n            clientId: this._collabClientId,\n            clientAvatarUrl: this._collabClientAvatarUrl,\n        };\n    }\n\n    clean() {\n        this.observerUnactive();\n        this.cleanForSave();\n        this.observerActive();\n    }\n\n    /**\n     * Initialize the provided element to be ready for edition.\n     */\n    initElementForEdition(element = this.editable) {\n        // Detect if the editable base element contain orphan inline nodes. If\n        // so we transform the base element HTML to put those orphans inside\n        // `<p>` containers.\n        const orphanInlineChildNodes = [...element.childNodes].find(\n            (n) => !isBlock(n) && (n.nodeType === Node.ELEMENT_NODE || n.textContent.trim() !== \"\")\n        );\n        if (orphanInlineChildNodes && !this.options.allowInlineAtRoot) {\n            const childNodes = [...element.childNodes];\n            const tempEl = document.createElement('temp-container');\n            let currentP = document.createElement('p');\n            currentP.style.marginBottom = '0';\n            do {\n                const node = childNodes.shift();\n                const nodeIsBlock = isBlock(node);\n                const nodeIsBR = node.nodeName === 'BR';\n                // Append to the P unless child is block or an unneeded BR.\n                if (!(nodeIsBlock || (nodeIsBR && currentP.childNodes.length))) {\n                    currentP.append(node);\n                }\n                // Break paragraphs on blocks and BR.\n                if (nodeIsBlock || nodeIsBR || childNodes.length === 0) {\n                    // Ensure we don't add an empty P or a P containing only\n                    // formating spaces that should not be visible.\n                    if (currentP.childNodes.length && currentP.innerHTML.trim() !== '') {\n                        tempEl.append(currentP);\n                    }\n                    currentP = currentP.cloneNode();\n                    // Append block children directly to the template.\n                    if (nodeIsBlock) {\n                        tempEl.append(node);\n                    }\n                }\n            } while (childNodes.length)\n            element.replaceChildren(...tempEl.childNodes);\n        }\n\n        // Flag elements with forced contenteditable=false.\n        // We need the flag to be able to leave the contentEditable\n        // at the end of the edition (see cleanForSave())\n        for (const el of element.querySelectorAll('[contenteditable=\"false\"]')) {\n            el.setAttribute('data-oe-keep-contenteditable', '');\n        }\n        // Flag elements .oe-tabs contenteditable=false.\n        for (const el of element.querySelectorAll('.oe-tabs')) {\n            el.setAttribute('contenteditable', 'false');\n        }\n    }\n\n    cleanForSave(element = this.editable) {\n        for (const hint of element.querySelectorAll('.oe-hint')) {\n            hint.classList.remove('oe-hint', 'oe-command-temporary-hint');\n            if (hint.classList.length === 0) {\n                hint.removeAttribute('class');\n            }\n            hint.removeAttribute('placeholder');\n        }\n        this._pluginCall('cleanForSave', [element]);\n\n        // Clean the zero-width spaces added by the `fillEmpty` function\n        // (flagged with the \"data-oe-zws-empty-inline\" attributes). Reverse the\n        // list to start from the deepest elements (for emptiness checks).\n        const allWhitespaceRegex = /^[\\s\\u200b]*$/;\n        for (const emptyElement of [...element.querySelectorAll('[data-oe-zws-empty-inline]')].reverse()) {\n            emptyElement.removeAttribute('data-oe-zws-empty-inline');\n            if (!allWhitespaceRegex.test(emptyElement.textContent)) {\n                // The element has some meaningful text. Remove the ZWS in it.\n                cleanZWS(emptyElement);\n            } else if (!emptyElement.classList.length) {\n                // We only remove the empty element if it has no class, to\n                // ensure we don't break visual styles (in that case, its\n                // ZWS was kept to ensure the cursor can be placed in it).\n                emptyElement.remove();\n            }\n        }\n\n        // Clean all transient nodes\n        const protectedNodes = element.querySelectorAll('[data-oe-transient-content=\"true\"], [data-oe-transient-content=\"\"]');\n        for (const node of protectedNodes) {\n            node.replaceChildren();\n        }\n\n        sanitize(element);\n\n        // Remove o_link_in_selection class\n        for (const link of element.querySelectorAll('.o_link_in_selection')) {\n            link.classList.remove('o_link_in_selection');\n        }\n\n        // Remove all FEFF within a `prepareUpdate` to make sure to make <br>\n        // nodes visible if needed.\n        for (const node of descendants(element)) {\n            if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('\\uFEFF')) {\n                const restore = prepareUpdate(...leftPos(node));\n                node.textContent = node.textContent.replaceAll('\\uFEFF', '');\n                restore(); // Make sure to make <br>s visible if needed.\n            }\n        }\n        // Remove now empty links\n        for (const link of element.querySelectorAll('a')) {\n            if (![...link.childNodes].some(isVisible) && !link.classList.length) {\n                link.remove();\n            }\n        }\n\n        // Remove contenteditable=false on elements\n        for (const el of element.querySelectorAll('[contenteditable=\"false\"]')) {\n            if (!el.hasAttribute('data-oe-keep-contenteditable')) {\n                el.removeAttribute('contenteditable');\n            }\n        }\n        // Remove data-oe-keep-contenteditable on elements\n        for (const el of element.querySelectorAll('[data-oe-keep-contenteditable]')) {\n            el.removeAttribute('data-oe-keep-contenteditable');\n        }\n\n        // Remove Zero Width Spaces on Font awesome elements\n        for (const el of element.querySelectorAll(ICON_SELECTOR)) {\n            cleanZWS(el);\n        }\n\n        // Clean custom selections\n        if (this.deselectTable() && hasValidSelection(this.editable)) {\n            this.document.getSelection().collapseToStart();\n        }\n\n        // Remove empty class attributes\n        for (const el of element.querySelectorAll('*[class=\"\"]')) {\n            el.removeAttribute('class');\n        }\n    }\n    /**\n     * Handle the hint preview for the Powerbox.\n     * @private\n     */\n    _handleCommandHint() {\n        const selection = this.document.getSelection();\n        const anchorNode = selection.anchorNode;\n        if (isProtected(anchorNode)) {\n            return;\n        }\n\n        const selectors = {\n            BLOCKQUOTE: this.options._t('Empty quote'),\n            H1: this.options._t('Heading 1'),\n            H2: this.options._t('Heading 2'),\n            H3: this.options._t('Heading 3'),\n            H4: this.options._t('Heading 4'),\n            H5: this.options._t('Heading 5'),\n            H6: this.options._t('Heading 6'),\n            LI: this.options._t('List'),\n        };\n\n        for (const hint of this.editable.querySelectorAll('.oe-hint')) {\n            if (\n                hint.classList.contains('oe-command-temporary-hint') ||\n                !isEmptyBlock(hint) ||\n                hint.querySelector('T[t-out]')\n            ) {\n                this.observerUnactive();\n                hint.classList.remove('oe-hint', 'oe-command-temporary-hint');\n                if (hint.dataset.oeEditPlaceholder) {\n                    hint.setAttribute(\"placeholder\", hint.dataset.oeEditPlaceholder);\n                    if (hint.innerText.trim().length === 0) {\n                        hint.classList.add(\"oe-hint\");\n                    }\n                } else {\n                    hint.removeAttribute(\"placeholder\");\n                }\n                if (hint.classList.length === 0) {\n                    hint.removeAttribute('class');\n                }\n                this.observerActive();\n            }\n        }\n\n        const block = this.options.getPowerboxElement();\n        if (block && !this.options.isHintBlacklisted(block)) {\n            if (block.nodeName in selectors && this.options.showEmptyElementHint) {\n                this._makeHint(block, selectors[block.nodeName], true);\n            } else if (block.nodeName === 'P' || block.nodeName === 'DIV') {\n                this._makeHint(block, this.options._t('Type \"/\" for commands'), true);\n            }\n        }\n\n        // placeholder hint\n        const sel = this.document.getSelection();\n        if (this.editable.textContent.trim() === '' && this.options.placeholder && this.editable.firstChild && this.editable.firstChild.innerHTML && !this.editable.contains(sel.focusNode)) {\n            this._makeHint(this.editable.firstChild, this.options.placeholder, true);\n        }\n    }\n    _makeHint(block, text, temporary = false) {\n        const content = block && block.innerHTML.trim();\n        if (\n            block &&\n            (content === '' || content === '<br>') &&\n            !block.querySelector('T[t-out],[t-field]') &&\n            ancestors(block, this.editable).includes(this.editable)\n        ) {\n            this.observerUnactive();\n            block.setAttribute('placeholder', text);\n            block.classList.add('oe-hint');\n            if (temporary) {\n                block.classList.add('oe-command-temporary-hint');\n            }\n            this.observerActive();\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     * @param {String} currentKeyPress\n     */\n    _fixSelectionOnEditableRoot(selection, currentKeyPress) {\n        if (!this.editable.isContentEditable) {\n            return;\n        }\n        let nodeAfterCursor = this.editable.childNodes[selection.anchorOffset];\n        let nodeBeforeCursor = nodeAfterCursor && nodeAfterCursor.previousElementSibling;\n        // Handle arrow key presses.\n        if (currentKeyPress === 'ArrowRight' || currentKeyPress === 'ArrowDown') {\n            while (nodeAfterCursor && isNotAllowedContent(nodeAfterCursor)) {\n                nodeAfterCursor = nodeAfterCursor.nextElementSibling;\n            }\n            if (nodeAfterCursor) {\n                setSelection(...getDeepestPosition(nodeAfterCursor, 0));\n            } else {\n                this.historyResetLatestComputedSelection(true);\n            }\n        } else if (currentKeyPress === 'ArrowLeft' || currentKeyPress === 'ArrowUp') {\n            while (nodeBeforeCursor && isNotAllowedContent(nodeBeforeCursor)) {\n                nodeBeforeCursor = nodeBeforeCursor.previousElementSibling;\n            }\n            if (nodeBeforeCursor) {\n                setSelection(...getDeepestPosition(nodeBeforeCursor, nodeSize(nodeBeforeCursor)));\n            } else {\n                this.historyResetLatestComputedSelection(true);\n            }\n        // Handle cursor next to a 'P'.\n        } else if (nodeAfterCursor && paragraphRelatedElements.includes(nodeAfterCursor.nodeName)) {\n            // Cursor is right before a 'P'.\n            setCursorStart(nodeAfterCursor);\n        } else if (nodeBeforeCursor && paragraphRelatedElements.includes(nodeBeforeCursor.nodeName)) {\n            // Cursor is right after a 'P'.\n            setCursorEnd(nodeBeforeCursor);\n        // Handle cursor not next to a 'P'.\n        // Insert a new 'P' if selection resulted from a mouse click.\n        } else if (this._currentMouseState === 'mousedown') {\n            this._recordHistorySelection(true);\n            const p = this.document.createElement('p');\n            p.append(this.document.createElement('br'));\n            if (!nodeAfterCursor) {\n                // Cursor is at the end of the editable.\n                this.editable.append(p);\n            } else if (!nodeBeforeCursor) {\n                // Cursor is at the beginning of the editable.\n                this.editable.prepend(p);\n            } else {\n                // Cursor is between two non-p blocks\n                nodeAfterCursor.before(p);\n            }\n            setCursorStart(p);\n            this.historyStep();\n        } else {\n            // Remove selection as a fallback.\n            selection.removeAllRanges();\n        }\n    }\n\n    _onMouseup(ev) {\n        this._currentMouseState = ev.type;\n\n        this._fixFontAwesomeSelection();\n    }\n\n    _onMouseDown(ev) {\n        this._currentMouseState = ev.type;\n        this._lastMouseClickPosition = [ev.x, ev.y];\n\n        if (this.canActivateContentEditable) {\n            this._activateContenteditable();\n        }\n\n        // Ignore any changes that might have happened before this point.\n        this.observer.takeRecords();\n\n        // Reset selection when editable is empty.\n        const selection = this.document.getSelection();\n        if (!selection.isCollapsed) {\n            const range = selection.getRangeAt(0);\n            const rangeContentChildNodes = range.cloneContents().childNodes;\n            if (rangeContentChildNodes.length === 1 && rangeContentChildNodes[0].nodeName === 'BR') {\n                setSelection(selection.anchorNode, 0, selection.anchorNode, 0);\n            }\n        }\n\n        const node = ev.target;\n        // handle checkbox lists\n        if (node.tagName == 'LI' && getListMode(node.parentElement) == 'CL') {\n            const beforStyle = window.getComputedStyle(node, ':before');\n            const style1 = {\n                left: parseInt(beforStyle.getPropertyValue('left'), 10),\n                top: parseInt(beforStyle.getPropertyValue('top'), 10),\n            }\n            style1.right = style1.left + parseInt(beforStyle.getPropertyValue('width'), 10);\n            style1.bottom = style1.top + parseInt(beforStyle.getPropertyValue('height'), 10);\n\n            const isMouseInsideCheckboxBox =\n                ev.offsetX >= style1.left &&\n                ev.offsetX <= style1.right &&\n                ev.offsetY >= style1.top &&\n                ev.offsetY <= style1.bottom;\n\n            if (isMouseInsideCheckboxBox) {\n                toggleClass(node, 'o_checked');\n                this.historyStep();\n                if (!document.getSelection().isCollapsed) {\n                    this._updateToolbar(true);\n                }\n            }\n        }\n\n        // handle stars\n        const isStar = el => el.nodeType === Node.ELEMENT_NODE && (\n            el.classList.contains('fa-star') || el.classList.contains('fa-star-o')\n        );\n        if (isStar(node) &&\n            node.parentElement && node.parentElement.className.includes('o_stars')) {\n            const previousStars = getAdjacentPreviousSiblings(node, isStar);\n            const nextStars = getAdjacentNextSiblings(node, isStar);\n            if (nextStars.length || previousStars.length) {\n                const shouldToggleOff = 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.historyStep();\n            }\n        }\n\n        // Handle table selection.\n        if (this.toolbar && !ancestors(ev.target, this.editable).includes(this.toolbar)) {\n            this.toolbar.style.pointerEvents = 'none';\n            if (this.deselectTable() && hasValidSelection(this.editable)) {\n                this.document.getSelection().collapseToStart();\n                this._updateToolbar(false);\n            }\n        }\n        // Handle table resizing.\n        const isHoveringTdBorder = this._isHoveringTdBorder(ev);\n        const isRTL = this.options.direction === 'rtl';\n        if (isHoveringTdBorder) {\n            ev.preventDefault();\n            const direction = { top: 'row', right: 'col', bottom: 'row', left: 'col' }[isHoveringTdBorder] || false;\n            let target1, target2;\n            const column = closestElement(ev.target, 'tr');\n            if (isHoveringTdBorder === 'top' && column) {\n                target1 = getAdjacentPreviousSiblings(column).find(node => node.nodeName === 'TR');\n                target2 = closestElement(ev.target, 'tr');\n            } else if (isHoveringTdBorder === 'right') {\n                if (isRTL) {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(node => node.nodeName === 'TD');\n                    target2 = ev.target;\n                } else {\n                    target1 = ev.target;\n                    target2 = getAdjacentNextSiblings(ev.target).find(node => node.nodeName === 'TD');\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(node => node.nodeName === 'TD');\n                } else {\n                    target1 = getAdjacentPreviousSiblings(ev.target).find(node => node.nodeName === 'TD');\n                    target2 = ev.target;\n                }\n            }\n            this._isResizingTable = true;\n            this._toggleTableResizeCursor(direction);\n            const resizeTable = ev => this._resizeTable(ev, direction, target1, target2);\n            const stopResizing = ev => {\n                ev.preventDefault();\n                this._isResizingTable = false;\n                this._toggleTableResizeCursor(false);\n                this.historyStep();\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        // Handle emoji popover\n        const isEmojiPopover = document.querySelector('.o-EmojiPicker');\n        if (isEmojiPopover && ev.target !== isEmojiPopover) {\n            isEmojiPopover.remove();\n        }\n    }\n\n    _onScroll(ev) {\n        if (this._rowUiTarget && !this._rowUi.classList.contains('o_open')) {\n            this._positionTableUi(this._rowUiTarget);\n        }\n        if (this._columnUiTarget && !this._columnUi.classList.contains('o_open')) {\n            this._positionTableUi(this._columnUiTarget);\n        }\n    }\n\n    _onDocumentKeydown(ev) {\n        const canUndoRedo = !['INPUT', 'TEXTAREA'].includes(this.document.activeElement.tagName);\n\n        if (this.options.controlHistoryFromDocument && canUndoRedo) {\n            if (IS_KEYBOARD_EVENT_UNDO(ev) && canUndoRedo) {\n                ev.preventDefault();\n                this.historyUndo();\n            } else if (IS_KEYBOARD_EVENT_REDO(ev) && canUndoRedo) {\n                ev.preventDefault();\n                this.historyRedo();\n            }\n        } else {\n            if (IS_KEYBOARD_EVENT_REDO(ev) || IS_KEYBOARD_EVENT_UNDO(ev)) {\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                    this.automaticStepSkipStack();\n                    node.setAttribute('contenteditable', false);\n                }\n            }\n        }\n    }\n\n    _onDocumentKeyup() {\n        if (this._onKeyupResetContenteditableNodes.length) {\n            for (const node of this._onKeyupResetContenteditableNodes) {\n                this.automaticStepSkipStack();\n                node.setAttribute('contenteditable', true);\n            }\n            this._onKeyupResetContenteditableNodes = [];\n        }\n    }\n\n    _onDocumentMouseup(ev) {\n        this._currentMouseState = ev.type;\n        if (this.toolbar) {\n            this.toolbar.style.pointerEvents = 'auto';\n        }\n    }\n\n    _onMousemove(ev) {\n        if (this._currentMouseState === 'mousedown' && !this._isResizingTable) {\n            this._handleSelectionInTable(ev);\n        }\n        if (!this._rowUi.classList.contains('o_open') && !this._columnUi.classList.contains('o_open')) {\n            const column = closestElement(ev.target, 'td');\n            if (this._isResizingTable || !column || !column.isContentEditable || !ev.target || ev.target.nodeType !== Node.ELEMENT_NODE) {\n                this._toggleTableUi(false, false);\n            } else {\n                const row = closestElement(column, 'tr');\n                const isFirstColumn = column === row.querySelector('td');\n                const table = column && closestElement(column, 'table');\n                const isFirstRow = table && row === table.querySelector('tr');\n                this._toggleTableUi(isFirstColumn && row, isFirstRow && column);\n            }\n        }\n        const direction = {top: 'row', right: 'col', bottom: 'row', left: 'col'}[this._isHoveringTdBorder(ev)] || false;\n        if (direction || !this._isResizingTable) {\n            this._toggleTableResizeCursor(direction);\n        }\n    }\n\n    _onMouseLeave(ev) {\n        if (!this._isResizingTable) {\n            this._toggleTableResizeCursor(false);\n        }\n    }\n\n    _onDocumentClick(ev) {\n        // Close Table UI.\n        this._rowUi.classList.remove('o_open');\n        this._columnUi.classList.remove('o_open');\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        const selection = this.document.getSelection();\n        if (\n            selection &&\n            selection.anchorNode &&\n            isHtmlContentSupported(selection.anchorNode) &&\n            !closestElement(selection.anchorNode).closest('a') &&\n            selection.anchorNode.nodeType === Node.TEXT_NODE\n        ) {\n            // Merge adjacent text nodes.\n            selection.anchorNode.parentNode.normalize();\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(selection.anchorOffset);\n                const url = match[2] ? match[0] : 'http://' + match[0];\n                const range = this.document.createRange();\n                const startOffset = selection.anchorOffset - potentialUrl.length + match.index;\n                range.setStart(selection.anchorNode, startOffset);\n                range.setEnd(selection.anchorNode, startOffset + match[0].length);\n                const link = this._createLink(range.extractContents().textContent, url);\n                range.insertNode(link);\n                setCursorStart(nodeForSelectionRestore, false);\n            }\n        }\n    }\n\n    /**\n     * @param {String} label\n     * @param {String} url\n     */\n    _createLink(label, url) {\n        const link = this.document.createElement('a');\n        link.setAttribute('href', url);\n        for (const [param, value] of Object.entries(this.options.defaultLinkAttributes)) {\n            link.setAttribute(param, `${value}`);\n        }\n        link.innerText = label;\n        return link;\n    }\n    /**\n     * Add images inside the editable at the current selection.\n     *\n     * @param {File[]} imageFiles\n     */\n    addImagesFiles(imageFiles) {\n        const promises = [];\n        for (const imageFile of imageFiles) {\n            const imageNode = document.createElement('img');\n            imageNode.classList.add('img-fluid');\n            // Mark images as having to be saved as attachments.\n            if (this.options.dropImageAsAttachment) {\n                imageNode.classList.add('o_b64_image_to_save');\n            }\n            imageNode.dataset.fileName = imageFile.name;\n            promises.push(getImageUrl(imageFile).then(url => {\n                imageNode.src = url;\n                return imageNode;\n            }));\n        }\n        return Promise.all(promises).then(nodes => {\n            const fragment = document.createDocumentFragment();\n            fragment.append(...nodes);\n            return fragment;\n        });\n    }\n    /**\n     * Handle safe pasting of html or plain text into the editor.\n     */\n    _onPaste(ev) {\n        const sel = this.document.getSelection();\n        if (sel.anchorNode && isProtected(sel.anchorNode)) {\n            return;\n        }\n        ev.preventDefault();\n        const files = getImageFiles(ev.clipboardData);\n        const odooEditorHtml = ev.clipboardData.getData('text/odoo-editor');\n        const clipboardHtml = ev.clipboardData.getData('text/html');\n        const targetSupportsHtmlContent = isHtmlContentSupported(sel.anchorNode);\n        // Replace entire link if its label is fully selected.\n        const link = closestElement(sel.anchorNode, 'a');\n        if (link && sel.toString().replace(ZERO_WIDTH_CHARS_REGEX, '') === link.innerText.replace(ZERO_WIDTH_CHARS_REGEX, '')) {\n            const start = leftPos(link);\n            link.remove();\n            setSelection(...start, ...start, false);\n        }\n        if (!targetSupportsHtmlContent) {\n            const text = ev.clipboardData.getData(\"text/plain\");\n            this._applyCommand(\"insert\", text);\n        } else if (odooEditorHtml) {\n            const fragment = parseHTML(this.document, odooEditorHtml);\n            const selector = this.options.renderingClasses.map(c => `.${c}`).join(',');\n            if (selector) {\n                for (const element of fragment.querySelectorAll(selector)) {\n                    element.classList.remove(...this.options.renderingClasses);\n                }\n            }\n            // Instantiate DOMPurify with the correct window.\n            this.DOMPurify ??= DOMPurify(this.document.defaultView,);\n            this.DOMPurify.sanitize(fragment, {\n                IN_PLACE: true,\n                ADD_TAGS: [\"#document-fragment\", \"fake-el\"],\n                ADD_ATTR: [\"contenteditable\"],\n            });\n            if (fragment.hasChildNodes()) {\n                this._applyCommand('insert', fragment);\n            }\n        } else if (files.length || clipboardHtml) {\n            const clipboardElem = this._prepareClipboardData(clipboardHtml);\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                this.addImagesFiles(files).then(html => {\n                    this._applyCommand('insert', html);\n                });\n            } else {\n                if (closestElement(sel.anchorNode, 'a')) {\n                    this._applyCommand('insert', clipboardElem.textContent);\n                }\n                else {\n                    this._applyCommand('insert', clipboardElem);\n                }\n            }\n        } else {\n            const text = ev.clipboardData.getData('text/plain');\n            const selectionIsInsideALink = !!closestElement(sel.anchorNode, 'a');\n            const isSelectionInsidePre = !!closestElement(sel.anchorNode, 'pre');\n            let splitAroundUrl = [text];\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 (splitAroundUrl.length === 3 && !splitAroundUrl[0] && !splitAroundUrl[2] && !isSelectionInsidePre) {\n                // Pasted content is a single URL.\n                const url = /^https?:\\/\\//i.test(text) ? text : 'http://' + text;\n                const youtubeUrl = this.options.allowCommandVideo && YOUTUBE_URL_GET_VIDEO_ID.exec(url);\n                const urlFileExtention = url.split('.').pop();\n                const isImageUrl = ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(urlFileExtention.toLowerCase());\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 (selectionIsInsideALink) {\n                    if (isImageUrl) {\n                        const img = document.createElement('IMG');\n                        img.setAttribute('src', url);\n                        this._applyCommand('insert', img);\n                    } else {\n                        this._applyCommand('insert', text);\n                    }\n                } else if (isImageUrl || youtubeUrl) {\n                    // Open powerbox with commands to embed media or paste as link.\n                    // Store history step index to revert it later.\n                    const stepIndexBeforeInsert = this._historySteps.length - 1;\n                    // Store mutations before text insertion, to reapply them after history revert.\n                    this.observerFlush();\n                    const currentStepMutations = [...this._currentStep.mutations];\n                    // Insert URL as text, revert it later.\n                    this._applyCommand('insert', text);\n                    const revertTextInsertion = () => {\n                        this.historyRevertUntil(stepIndexBeforeInsert);\n                        this.historyStep(true);\n                        this._historyStepsStates.set(peek(this._historySteps).id, 'consumed');\n                        // Reapply mutations that were done before the text insertion.\n                        this.historyApply(currentStepMutations);\n                    };\n                    let commands;\n                    const pasteAsURLCommand = {\n                        name: this.options._t('Paste as URL'),\n                        description: this.options._t('Create an URL.'),\n                        fontawesome: 'fa-link',\n                        callback: () => {\n                            revertTextInsertion();\n                            this._applyRawCommand('insert', this._createLink(text, url))\n                        },\n                    };\n                    if (isImageUrl) {\n                        const embedImageCommand = {\n                            name: this.options._t('Embed Image'),\n                            description: this.options._t('Embed the image in the document.'),\n                            fontawesome: 'fa-image',\n                            callback: () => {\n                                revertTextInsertion();\n                                const img = document.createElement('IMG');\n                                img.setAttribute('src', url);\n                                this._applyRawCommand('insert', img);\n                            },\n                        };\n                        commands = [embedImageCommand, pasteAsURLCommand];\n                    } else {\n                         // URL is a YouTube video.\n                        const embedVideoCommand = {\n                            name: this.options._t('Embed Youtube Video'),\n                            description: this.options._t('Embed the youtube video in the document.'),\n                            fontawesome: 'fa-youtube-play',\n                            callback: async () => {\n                                revertTextInsertion();\n                                let videoElement;\n                                if (this.options.getYoutubeVideoElement) {\n                                    videoElement = await this.options.getYoutubeVideoElement(youtubeUrl[0]);\n                                } else {\n                                    videoElement = document.createElement('iframe');\n                                    videoElement.setAttribute('width', '560');\n                                    videoElement.setAttribute('height', '315');\n                                    videoElement.setAttribute(\n                                        'src',\n                                        `https://www.youtube.com/embed/${encodeURIComponent(youtubeUrl[1])}`,\n                                    );\n                                    videoElement.setAttribute('title', 'YouTube video player');\n                                    videoElement.setAttribute('frameborder', '0');\n                                    videoElement.setAttribute(\n                                        'allow',\n                                        'accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture',\n                                    );\n                                    videoElement.setAttribute('allowfullscreen', '1');\n                                }\n                                this._applyRawCommand('insert', videoElement);\n                            },\n                        };\n                        commands = [embedVideoCommand, pasteAsURLCommand];\n                    }\n                    this.powerbox.open(commands);\n                } else {\n                    this._applyCommand('insert', this._createLink(text, url));\n                }\n            } else {\n                this.historyPauseSteps();\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 && !isSelectionInsidePre) {\n                        this._applyCommand('insert', this._createLink(splitAroundUrl[i], url));\n                    } else if (splitAroundUrl[i] !== '') {\n                        const textFragments = splitAroundUrl[i].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._applyCommand('insert', modifiedTextFragment);\n                            if (textIndex < textFragments.length) {\n                                // Break line by inserting new paragraph and\n                                // remove current paragraph's bottom margin.\n                                const p = closestElement(sel.anchorNode, 'p');\n                                if (\n                                    isUnbreakable(closestBlock(sel.anchorNode)) ||\n                                    closestElement(sel.anchorNode).nodeName === 'PRE'\n                                ) {\n                                    this._applyCommand('oShiftEnter');\n                                } else {\n                                    this._applyCommand('oEnter');\n                                    p && (p.style.marginBottom = '0px');\n                                }\n                            }\n                            textIndex++;\n                        }\n                    }\n                }\n                this.historyUnpauseSteps();\n                this.historyStep();\n            }\n        }\n    }\n    _onDragStart(ev) {\n        if (ev.target.nodeName === 'IMG') {\n            ev.dataTransfer.setData('text/plain', `oid:${ev.target.oid}`);\n        }\n    }\n    /**\n     * Handle safe dropping of html into the editor.\n     */\n    _onDrop(ev) {\n        ev.preventDefault();\n        if (!isHtmlContentSupported(ev.target)) {\n            return;\n        }\n        const sel = this.document.getSelection();\n        let isInEditor = false;\n        let ancestor = sel.anchorNode;\n        while (ancestor && !isInEditor) {\n            if (ancestor === this.editable) {\n                isInEditor = true;\n            }\n            ancestor = ancestor.parentNode;\n        }\n        const dataTransfer = (ev.originalEvent || ev).dataTransfer;\n        const imageOidMatch = (dataTransfer.getData('text') || '').match('oid:(.*)');\n        const imageOid = imageOidMatch && imageOidMatch[1];\n        const image = imageOid && [...this.editable.querySelectorAll('*')].find(\n            node => node.oid === imageOid,\n        );\n        const fileTransferItems = getImageFiles(dataTransfer);\n        const htmlTransferItem = [...dataTransfer.items].find(\n            item => item.type === 'text/html',\n        );\n        if (image || fileTransferItems.length || htmlTransferItem) {\n            if (this.document.caretPositionFromPoint) {\n                const range = this.document.caretPositionFromPoint(ev.clientX, ev.clientY);\n                setSelection(range.offsetNode, range.offset);\n            } else if (this.document.caretRangeFromPoint) {\n                const range = this.document.caretRangeFromPoint(ev.clientX, ev.clientY);\n                setSelection(range.startContainer, range.startOffset);\n            }\n        }\n        if (image) {\n            image.classList.toggle('img-fluid', true);\n            const html = image.outerHTML;\n            image.remove();\n            this.execCommand('insert', this._prepareClipboardData(html));\n        } else if (fileTransferItems.length) {\n            this.addImagesFiles(fileTransferItems).then(html => {\n                this.execCommand('insert', html);\n            });\n        } else if (htmlTransferItem) {\n            htmlTransferItem.getAsString(pastedText => {\n                this.execCommand('insert', this._prepareClipboardData(pastedText));\n            });\n        }\n        this.historyStep();\n    }\n\n    _onTabulationInTable(ev) {\n        const sel = this.document.getSelection();\n        const closestTable = closestElement(sel.anchorNode, 'table');\n        if (!closestTable) {\n            return;\n        }\n        const closestTd = closestElement(sel.anchorNode, 'td');\n        const tds = [...closestTable.querySelectorAll('td')];\n        const direction = ev.shiftKey ? DIRECTIONS.LEFT : DIRECTIONS.RIGHT;\n        const cursorDestination =\n            tds[tds.findIndex(td => closestTd === td) + (direction === DIRECTIONS.LEFT ? -1 : 1)];\n        if (cursorDestination) {\n            setCursorEnd(lastLeaf(cursorDestination));\n        } else if (direction === DIRECTIONS.RIGHT) {\n            this.execCommand('addRow', 'after');\n            this._onTabulationInTable(ev);\n        }\n    }\n    _onTableMenuTogglerClick(ev) {\n        const uiWrapper = ev.target.closest('.o_table_ui');\n        uiWrapper.classList.toggle('o_open');\n\n        if (this.options.direction === 'rtl') {\n            const menuRowEl = this._tableUiContainer.querySelector('.o_row_ui .o_table_ui_menu')\n            const menuRowRect = menuRowEl.getBoundingClientRect();\n            menuRowEl.style.position = 'absolute';\n            menuRowEl.style.left = `-${menuRowRect.width}px`;\n            menuRowEl.style.margin = `0px`;\n        }\n\n        if (uiWrapper.classList.contains('o_column_ui')) {\n            const columnIndex = getColumnIndex(this._columnUiTarget);\n            uiWrapper.querySelector('.o_move_left').classList.toggle('o_hide', columnIndex === 0);\n            const shouldHideRight = columnIndex === [...this._columnUiTarget.parentElement.children].filter(child => child.nodeName === 'TD').length - 1;\n            uiWrapper.querySelector('.o_move_right').classList.toggle('o_hide', shouldHideRight);\n        } else {\n            const rowIndex = getRowIndex(this._rowUiTarget);\n            uiWrapper.querySelector('.o_move_up').classList.toggle('o_hide', rowIndex === 0);\n            const shouldHideDown = rowIndex === [...this._rowUiTarget.parentElement.children].filter(child => child.nodeName === 'TR').length - 1;\n            uiWrapper.querySelector('.o_move_down').classList.toggle('o_hide', shouldHideDown);\n        }\n        ev.stopPropagation();\n    }\n    _onTableMoveUpClick() {\n        if (this._rowUiTarget.previousSibling) {\n            // When moving the second row up, copy the widths of first row's td\n            // elements to second row's td elements, as td widths are only\n            // applied to the first row.\n            if (!this._rowUiTarget.previousSibling.previousSibling) {\n                this._rowUiTarget.childNodes.forEach((cell, index) => {\n                    cell.style.width = this._rowUiTarget.previousSibling.childNodes[index].style.width;\n                });\n            }\n            this._rowUiTarget.previousSibling.before(this._rowUiTarget);\n        }\n    }\n    _onTableMoveDownClick() {\n        if (this._rowUiTarget.nextSibling) {\n            // When moving the first row down, copy the widths of its td\n            // elements to second row's td elements, as td widths are only\n            // applied to the first row.\n            if (!this._rowUiTarget.previousSibling) {\n                this._rowUiTarget.nextSibling.childNodes.forEach((cell, index) => {\n                    cell.style.width = this._rowUiTarget.childNodes[index].style.width;\n                });\n            }\n            this._rowUiTarget.nextSibling.after(this._rowUiTarget);\n        }\n    }\n    _onTableMoveRightClick() {\n        const trs = [...this._columnUiTarget.parentElement.parentElement.children].filter(child => child.nodeName === 'TR');\n        const columnIndex = getColumnIndex(this._columnUiTarget);\n        const tdsToMove = trs.map(tr => [...tr.children].filter(child => child.nodeName === 'TD')[columnIndex]);\n        for (const tdToMove of tdsToMove) {\n            const target = [...tdToMove.parentElement.children].filter(child => child.nodeName === 'TD')[columnIndex + 1];\n            target.after(tdToMove);\n        }\n    }\n    _onTableMoveLeftClick() {\n        const trs = [...this._columnUiTarget.parentElement.parentElement.children].filter(child => child.nodeName === 'TR');\n        const columnIndex = getColumnIndex(this._columnUiTarget);\n        const tdsToMove = trs.map(tr => [...tr.children].filter(child => child.nodeName === 'TD')[columnIndex]);\n        for (const tdToMove of tdsToMove) {\n            const target = [...tdToMove.parentElement.children].filter(child => child.nodeName === 'TD')[columnIndex - 1];\n            target.before(tdToMove);\n        }\n    }\n    _onTableDeleteColumnClick() {\n        this.historyPauseSteps();\n        const rows = [...closestElement(this._columnUiTarget, 'tr').parentElement.children].filter(child => child.nodeName === 'TR');\n        this.execCommand('removeColumn', this._columnUiTarget);\n        if (rows.every(row => !row.parentElement)) {\n            this.execCommand('deleteTable', this.editable.querySelector('.o_selected_table'));\n        }\n        this.historyUnpauseSteps();\n        this.historyStep();\n    }\n    _onTableDeleteRowClick() {\n        this.historyPauseSteps();\n        const rows = [...this._rowUiTarget.parentElement.children].filter(child => child.nodeName === 'TR');\n        this.execCommand('removeRow', this._rowUiTarget);\n        if (rows.every(row => !row.parentElement)) {\n            this.execCommand('deleteTable', this.editable.querySelector('.o_selected_table'));\n        }\n        this.historyUnpauseSteps();\n        this.historyStep();\n    }\n\n    /**\n     * Fix the current selection range in case the range start or end inside a fontAwesome node\n     */\n    _fixFontAwesomeSelection() {\n        const selection = this.document.getSelection();\n        if (\n            selection.isCollapsed ||\n            (selection.anchorNode &&\n                !ancestors(selection.anchorNode, this.editable).includes(this.editable))\n        )\n            return;\n        let shouldUpdateSelection = false;\n        const fixedSelection = {\n            anchorNode: selection.anchorNode,\n            anchorOffset: selection.anchorOffset,\n            focusNode: selection.focusNode,\n            focusOffset: selection.focusOffset,\n        };\n        const selectionDirection = getCursorDirection(\n            selection.anchorNode,\n            selection.anchorOffset,\n            selection.focusNode,\n            selection.focusOffset,\n        );\n        // check and fix anchor node\n        const closestAnchorNodeEl = closestElement(selection.anchorNode);\n        if (isIconElement(closestAnchorNodeEl)) {\n            shouldUpdateSelection = true;\n            fixedSelection.anchorNode =\n                selectionDirection === DIRECTIONS.RIGHT\n                    ? closestAnchorNodeEl.previousSibling\n                    : closestAnchorNodeEl.nextSibling;\n            if (fixedSelection.anchorNode) {\n                fixedSelection.anchorOffset =\n                    selectionDirection === DIRECTIONS.RIGHT ? fixedSelection.anchorNode.length : 0;\n            } else {\n                fixedSelection.anchorNode = closestAnchorNodeEl.parentElement;\n                fixedSelection.anchorOffset = 0;\n            }\n        }\n        // check and fix focus node\n        const closestFocusNodeEl = closestElement(selection.focusNode);\n        if (isIconElement(closestFocusNodeEl)) {\n            shouldUpdateSelection = true;\n            fixedSelection.focusNode =\n                selectionDirection === DIRECTIONS.RIGHT\n                    ? closestFocusNodeEl.nextSibling\n                    : closestFocusNodeEl.previousSibling;\n            if (fixedSelection.focusNode) {\n                fixedSelection.focusOffset =\n                    selectionDirection === DIRECTIONS.RIGHT ? 0 : fixedSelection.focusNode.length;\n            } else {\n                fixedSelection.focusNode = closestFocusNodeEl.parentElement;\n                fixedSelection.focusOffset = 0;\n            }\n        }\n        if (shouldUpdateSelection) {\n            setSelection(\n                fixedSelection.anchorNode,\n                fixedSelection.anchorOffset,\n                fixedSelection.focusNode,\n                fixedSelection.focusOffset,\n                false,\n            );\n        }\n    }\n    _pluginAdd(Plugin) {\n        this._plugins.push(new Plugin({ editor: this }));\n    }\n    _pluginCall(method, args = []) {\n        for (const plugin of this._plugins) {\n            if (plugin[method]) {\n                plugin[method](...args);\n            }\n        }\n    }\n}\n", "/** @odoo-module **/\nexport const UNBREAKABLE_ROLLBACK_CODE = 'UNBREAKABLE';\nexport const UNREMOVABLE_ROLLBACK_CODE = 'UNREMOVABLE';\nexport const REGEX_BOOTSTRAP_COLUMN = /(?:^| )col(-[a-zA-Z]+)?(-\\d+)?(?:$| )/;\n", "/** @odoo-module **/\nimport {\n    closestBlock,\n    closestElement,\n    startPos,\n    getListMode,\n    isBlock,\n    isSelfClosingElement,\n    moveNodes,\n    preserveCursor,\n    isIconElement,\n    getDeepRange,\n    isUnbreakable,\n    isEditorTab,\n    isProtected,\n    isZWS,\n    isArtificialVoidElement,\n    ancestors,\n    EMAIL_REGEX,\n    PHONE_REGEX,\n    URL_REGEX,\n    unwrapContents,\n    padLinkWithZws,\n    getTraversedNodes,\n    ZERO_WIDTH_CHARS_REGEX,\n    isVisible,\n} from './utils.js';\n\nconst NOT_A_NUMBER = /[^\\d]/g;\n\n// In some cases, we want to prevent merging identical elements.\nexport const UNMERGEABLE_SELECTORS = [];\n\nfunction hasPseudoElementContent (node, pseudoSelector) {\n    const content = getComputedStyle(node, pseudoSelector).getPropertyValue('content');\n    return content && content !== 'none';\n}\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    const nodeName = node.nodeName;\n\n    for (const name of new Set([\n        ...node.getAttributeNames(),\n        ...node2.getAttributeNames(),\n    ])) {\n        if (node.getAttribute(name) !== node2.getAttribute(name)) {\n            return false; // The nodes don't have the same attributes.\n        }\n    }\n    if ([node, node2].some(n => hasPseudoElementContent(n, ':before') || hasPseudoElementContent(n, ':after'))) {\n        return false; // The nodes have pseudo elements with content.\n    }\n    if (isIconElement(node) || isIconElement(node2)) {\n        return false;\n    }\n    if (nodeName === 'LI' && node.classList.contains('oe-nested')) {\n        // If the nodes are adjacent nested list items, we need to compare the\n        // types of their \"adjacent\" list children rather that the list items\n        // themselves.\n        return (\n            node.lastElementChild &&\n            node2.firstElementChild &&\n            getListMode(node.lastElementChild) === getListMode(node2.firstElementChild)\n        );\n    }\n    if (['UL', 'OL'].includes(nodeName)) {\n        return !isSelfClosingElement(node) && !isSelfClosingElement(node2); // The nodes are non-empty lists. TODO: this doesn't check that and it will always be true!\n    }\n    if (isBlock(node) || isSelfClosingElement(node) || isSelfClosingElement(node2)) {\n        return false; // The nodes are blocks or are empty but visible. TODO: Not sure this was what we wanted to check (see just above).\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\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    // Skip modifying the href for Bootstrap tabs.\n    if (link && link.getAttribute(\"role\") === \"tab\") {\n        return;\n    }\n   const label = text.replace(ZERO_WIDTH_CHARS_REGEX, '').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\nfunction shouldPreserveCursor(node, root) {\n    const selection = root.ownerDocument.getSelection();\n    return node.isConnected && selection &&\n        selection.anchorNode && root.contains(selection.anchorNode) &&\n        selection.focusNode && root.contains(selection.focusNode);\n}\n\n/**\n * Sanitize the given node and return it.\n *\n * @param {Node} node\n * @param {Element} root\n * @returns {Node} the sanitized node\n */\nfunction sanitizeNode(node, root) {\n    // First ensure elements which should not contain any content are tagged\n    // contenteditable=false to avoid any hiccup.\n    if (isArtificialVoidElement(node) && node.getAttribute('contenteditable') !== 'false') {\n        node.setAttribute('contenteditable', 'false');\n    }\n\n    // Remove empty class/style attributes.\n    for (const attributeName of ['class', 'style']) {\n        if (node.nodeType === Node.ELEMENT_NODE && node.hasAttribute(attributeName) && !node.getAttribute(attributeName)) {\n            node.removeAttribute(attributeName);\n        }\n    }\n\n    if (\n        ['SPAN', 'FONT'].includes(node.nodeName)\n        && !node.hasAttributes()\n        && !hasPseudoElementContent(node, \"::before\")\n        && !hasPseudoElementContent(node, \"::after\")\n        && !node.querySelector(\".oe_currency_value\")\n    ) {\n        // Unwrap the contents of SPAN and FONT elements without attributes.\n        getDeepRange(root, { select: true });\n        const restoreCursor = shouldPreserveCursor(node, root) && preserveCursor(root.ownerDocument);\n        const parent = node.parentElement;\n        unwrapContents(node);\n        restoreCursor && restoreCursor();\n        node = parent; // The node has been removed, update the reference.\n    } else if (\n        areSimilarElements(node, node.previousSibling) &&\n        !isUnbreakable(node) &&\n        !isEditorTab(node) &&\n        !(\n            node.attributes?.length === 1 &&\n            node.hasAttribute('data-oe-zws-empty-inline') &&\n            (node.textContent === '\\u200B' || node.previousSibling.textContent === '\\u200B')\n        ) &&\n        !UNMERGEABLE_SELECTORS.some(selectorClass => node.classList?.contains(selectorClass))\n    ) {\n        // Merge identical elements together.\n        getDeepRange(root, { select: true });\n        const restoreCursor = shouldPreserveCursor(node, root) && preserveCursor(root.ownerDocument);\n        moveNodes(...startPos(node), node.previousSibling);\n        restoreCursor && restoreCursor();\n    } else if (node.nodeType === Node.COMMENT_NODE) {\n        // Remove comment nodes to avoid issues with mso comments.\n        const parent = node.parentElement;\n        node.remove();\n        node = parent; // The node has been removed, update the reference.\n    } else if (\n        node.nodeName === 'P' && // Note: not sure we should limit to <p>.\n        ['LI', 'A'].includes(node.parentElement.nodeName) &&\n        !node.parentElement.classList.contains('nav-item')\n    ) {\n        // Remove empty paragraphs in <li>.\n        const previous = node.previousSibling;\n        const attributes = node.attributes;\n        const parent = node.parentElement;\n        const restoreCursor = shouldPreserveCursor(node, root) && preserveCursor(root.ownerDocument);\n        if (attributes.length) {\n            const spanEl = document.createElement('span');\n            for (const attribute of attributes) {\n                spanEl.setAttribute(attribute.name, attribute.value);\n            }\n            if (spanEl.style.textAlign) {\n                // This is a tradeoff. Ideally, the state of the html\n                // after this function should be reachable by standard\n                // edition means and a span with display block is not.\n                // However, this is required in order to not break the\n                // design of already existing snippets.\n                spanEl.style.display = 'block';\n            }\n            spanEl.append(...node.childNodes);\n            node.replaceWith(spanEl);\n        } else {\n            unwrapContents(node);\n        }\n        if (previous && isVisible(previous) && !isBlock(previous) && previous.nodeName !== 'BR') {\n            const br = document.createElement('br');\n            previous.after(br);\n        }\n        restoreCursor && restoreCursor(new Map([[node, parent]]));\n        node = parent; // The node has been removed, update the reference.\n    } else if (node.nodeName === 'LI' && !node.closest('ul, ol')) {\n        // Transform <li> into <p> if they are not in a <ul> / <ol>.\n        const paragraph = document.createElement('p');\n        paragraph.replaceChildren(...node.childNodes);\n        node.replaceWith(paragraph);\n        node = paragraph; // The node has been removed, update the reference.\n    } else if (\n        ['UL', 'OL'].includes(node.nodeName) &&\n        ['UL', 'OL'].includes(node.parentNode.nodeName)\n    ) {\n        const restoreCursor = shouldPreserveCursor(node, root) && preserveCursor(root.ownerDocument);\n        const li = document.createElement('li');\n        node.parentNode.insertBefore(li, node);\n        li.appendChild(node);\n        li.classList.add('oe-nested');\n        node = li;\n        restoreCursor && restoreCursor();\n    } else if (isIconElement(node) && node.textContent !== '\\u200B') {\n        // Ensure a zero width space is present inside the FA element.\n        node.textContent = '\\u200B';\n    } else if (isEditorTab(node)) {\n        // Ensure the editor tabs align on a 40px grid.\n        let tabPreviousSibling = node.previousSibling;\n        while (isZWS(tabPreviousSibling)) {\n            tabPreviousSibling = tabPreviousSibling.previousSibling;\n        }\n        if (isEditorTab(tabPreviousSibling)) {\n            node.style.width = '40px';\n        } else {\n            const editable = closestElement(node, '.odoo-editor-editable');\n            if (editable?.firstElementChild) {\n                const nodeRect = node.getBoundingClientRect();\n                const referenceRect = editable.firstElementChild.getBoundingClientRect();\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 (nodeRect.width && referenceRect.width) {\n                    const width = (nodeRect.left - referenceRect.left) % 40;\n                    node.style.width = (40 - width) + 'px';\n                }\n            }\n        }\n    } else if (node.nodeName === 'A') {\n        // Ensure links have ZWNBSPs so the selection can be set at their edges.\n        padLinkWithZws(root, node);\n    } else if (\n        node.nodeType === Node.TEXT_NODE &&\n        node.textContent.includes('\\uFEFF') &&\n        !closestElement(node, 'a') &&\n        !(\n            closestElement(root, '[contenteditable=true]') &&\n            getTraversedNodes(closestElement(root, '[contenteditable=true]')).includes(node)\n        )\n    ) {\n        const startsWithLegitZws = node.textContent.startsWith('\\uFEFF') && node.previousSibling && node.previousSibling.nodeName === 'A';\n        const endsWithLegitZws = node.textContent.endsWith('\\uFEFF') && node.nextSibling && node.nextSibling.nodeName === 'A';\n        let newText = node.textContent.replace(/\\uFEFF/g, '');\n        if (startsWithLegitZws) {\n            newText = '\\uFEFF' + newText;\n        }\n        if (endsWithLegitZws) {\n            newText = newText + '\\uFEFF';\n        }\n        if (newText !== node.textContent) {\n            // We replace the text node with a new text node with the\n            // update text rather than just changing the text content of\n            // the node because these two methods create different\n            // mutations and at least the tour system breaks if all we\n            // send here is a text content change.\n            let replacement;\n            if (newText.length) {\n                replacement = document.createTextNode(newText);\n                node.before(replacement);\n            } else {\n                replacement = node.parentElement;\n            }\n            node.remove();\n            node = replacement; // The node has been removed, update the reference.\n        }\n    }\n    return node;\n}\n\n/**\n * Sanitize a node tree and return the sanitized node.\n *\n * @param {Node} nodeToSanitize the node to sanitize\n * @param {Node} [root] the root of the tree to sanitize (will not sanitize nodes outside of this tree)\n * @returns {Node} the sanitized node\n */\nexport function sanitize(nodeToSanitize, root = nodeToSanitize) {\n    const start = nodeToSanitize.ownerDocument.getSelection()?.anchorNode;\n    const block = closestBlock(nodeToSanitize);\n    if (block && root.contains(block)) {\n        // If the node is a list, start sanitization from its parent to ensure\n        // adjacent lists are merged when needed.\n        const isList = ['UL', 'OL'].includes(block.nodeName);\n        let node = isList ? block.parentElement : block;\n\n        // Sanitize the tree.\n        while (node && !(root.isConnected && !node.isConnected) && root.contains(node)) {\n            if (!isProtected(node)) {\n                node = sanitizeNode(node, root); // The node itself might be replaced during sanitization.\n            }\n            node = node.firstChild || node.nextSibling || ancestors(node, root).find(a => a.nextSibling)?.nextSibling;\n        }\n\n        // Ensure unique ids on checklists and stars.\n        const elementsWithId = [...block.querySelectorAll('[id^=checkId-]')];\n        const maxId = Math.max(...[0, ...elementsWithId.map(node => +node.getAttribute('id').substring(8))]);\n        let nextId = maxId + 1;\n        const ids = [];\n        for (const node of block.querySelectorAll('[id^=checkId-], .o_checklist > li, .o_stars')) {\n            if (\n                !node.classList.contains('o_stars') && (\n                    !node.parentElement.classList.contains('o_checklist') ||\n                    [...node.children].some(child => ['UL', 'OL'].includes(child.nodeName))\n            )) {\n                // Remove unique ids from checklists and stars from elements\n                // that are no longer checklist items or stars, and from\n                // parents of nested lists.\n                node.removeAttribute('id')\n            } else {\n                // Add/change IDs where needed, and ensure they're unique.\n                let id = node.getAttribute('id');\n                if (!id || ids.includes(id)) {\n                    id = `checkId-${nextId}`;\n                    nextId++;\n                    node.setAttribute('id', id);\n                }\n                ids.push(id);\n            }\n        }\n\n        // Update link URL if label is a new valid link.\n        const startEl = start && closestElement(start, 'a');\n        if (startEl && root.contains(startEl)) {\n            const label = startEl.innerText;\n            const url = deduceURLfromText(label, startEl);\n            if (url) {\n                startEl.setAttribute('href', url);\n            }\n        }\n    }\n    return nodeToSanitize;\n}\n", "/** @odoo-module **/\n// TODO: avoid empty keys when not necessary to reduce request size\nexport function serializeNode(node, nodesToStripFromChildren = new Set()) {\n    if (!node.oid) {\n        return;\n    }\n    const result = {\n        nodeType: node.nodeType,\n        oid: node.oid,\n    };\n    if (node.nodeType === Node.TEXT_NODE) {\n        result.textValue = node.nodeValue;\n    } else if (node.nodeType === Node.ELEMENT_NODE) {\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        let child = node.firstChild;\n        // Don't serialize transient nodes\n        if (![\"true\", \"\"].includes(node.dataset.oeTransientContent)) {\n            while (child) {\n                if (!nodesToStripFromChildren.has(child.oid)) {\n                    const serializedChild = serializeNode(child, nodesToStripFromChildren);\n                    if (serializedChild) {\n                        result.children.push(serializedChild);\n                    }\n                }\n                child = child.nextSibling;\n            }\n        }\n    }\n    return result;\n}\n\nexport function unserializeNode(obj) {\n    if (!obj) {\n        return;\n    }\n    let result = undefined;\n    if (obj.nodeType === Node.TEXT_NODE) {\n        result = document.createTextNode(obj.textValue);\n    } else if (obj.nodeType === Node.ELEMENT_NODE) {\n        result = document.createElement(obj.tagName);\n        for (const key in obj.attributes) {\n            result.setAttribute(key, obj.attributes[key]);\n        }\n        obj.children.forEach(child => result.append(unserializeNode(child)));\n    } else {\n        console.warn('unknown node type');\n    }\n    if (result) {\n        result.oid = obj.oid;\n        return result;\n    }\n}\n\nexport function serializeSelection(selection) {\n    if (\n        selection &&\n        selection.anchorNode &&\n        selection.anchorNode.oid &&\n        typeof selection.anchorOffset !==  'undefined' &&\n        selection.focusNode &&\n        selection.anchorNode.oid &&\n        typeof selection.focusOffset !==  'undefined'\n    ) {\n        return {\n            anchorNodeOid: selection.anchorNode.oid,\n            anchorOffset: selection.anchorOffset,\n            focusNodeOid: selection.focusNode.oid,\n            focusOffset: selection.focusOffset,\n        };\n    } else {\n        return {\n            anchorNodeOid: undefined,\n            anchorOffset: undefined,\n            focusNodeOid: undefined,\n            focusOffset: undefined,\n        };\n    }\n}\n", "/** @odoo-module **/\nimport { getRangePosition } from '../utils/utils.js';\n\nexport class TablePicker extends EventTarget {\n    constructor(options = {}) {\n        super();\n        this.options = options;\n        this.options.minRowCount = this.options.minRowCount || 3;\n        this.options.minColCount = this.options.minColCount || 3;\n        this.options.getContextFromParentRect = this.options.getContextFromParentRect || (() => ({ top: 0, left: 0 }));\n\n        this.rowNumber = this.options.minRowCount;\n        this.colNumber = this.options.minColCount;\n\n        this.tablePickerWrapper = document.createElement('div');\n        this.tablePickerWrapper.classList.add('oe-tablepicker-wrapper');\n        this.tablePickerWrapper.innerHTML = `\n            <div class=\"oe-tablepicker\"></div>\n            <div class=\"oe-tablepicker-size\"></div>\n        `;\n\n        if (this.options.floating) {\n            this.tablePickerWrapper.style.position = 'absolute';\n            this.tablePickerWrapper.classList.add('oe-floating');\n        }\n\n        this.tablePickerElement = this.tablePickerWrapper.querySelector('.oe-tablepicker');\n        this.tablePickerSizeViewElement =\n            this.tablePickerWrapper.querySelector('.oe-tablepicker-size');\n\n        this.el = this.tablePickerWrapper;\n\n        this.hide();\n    }\n\n    render() {\n        this.tablePickerElement.innerHTML = '';\n\n        const colCount = Math.max(this.colNumber, this.options.minRowCount);\n        const rowCount = Math.max(this.rowNumber, this.options.minRowCount);\n        const extraCol = 1;\n        const extraRow = 1;\n\n        for (let rowNumber = 1; rowNumber <= rowCount + extraRow; rowNumber++) {\n            const rowElement = document.createElement('div');\n            rowElement.classList.add('oe-tablepicker-row');\n            this.tablePickerElement.appendChild(rowElement);\n            for (let colNumber = 1; colNumber <= colCount + extraCol; colNumber++) {\n                const cell = this.el.ownerDocument.createElement('div');\n                cell.classList.add('oe-tablepicker-cell', 'btn');\n                rowElement.appendChild(cell);\n\n                if (rowNumber <= this.rowNumber && colNumber <= this.colNumber) {\n                    cell.classList.add('active');\n                }\n\n                const bindMouseMove = () => {\n                    cell.addEventListener('mouseover', () => {\n                        if (this.colNumber !== colNumber || this.rowNumber != rowNumber) {\n                            this.colNumber = colNumber;\n                            this.rowNumber = rowNumber;\n                            this.render();\n                        }\n                    });\n                    this.el.ownerDocument.removeEventListener('mousemove', bindMouseMove);\n                };\n                this.el.ownerDocument.addEventListener('mousemove', bindMouseMove);\n                cell.addEventListener('mousedown', this.selectCell.bind(this));\n            }\n        }\n\n        this.tablePickerSizeViewElement.textContent = `${this.colNumber}x${this.rowNumber}`;\n    }\n\n    show() {\n        this.reset();\n        this.el.style.display = 'block';\n        if (this.options.floating) {\n            this._showFloating();\n        }\n    }\n\n    hide() {\n        this.el.style.display = 'none';\n    }\n\n    reset() {\n        this.rowNumber = this.options.minRowCount;\n        this.colNumber = this.options.minColCount;\n        this.render();\n    }\n\n    selectCell() {\n        this.dispatchEvent(\n            new CustomEvent('cell-selected', {\n                detail: { colNumber: this.colNumber, rowNumber: this.rowNumber },\n            }),\n        );\n    }\n\n    _showFloating() {\n        const isRtl = this.options.direction === 'rtl';\n        const keydown = e => {\n            const actions = {\n                ArrowRight: {\n                    colNumber: (this.colNumber + (isRtl ? -1 : 1)) || 1,\n                    rowNumber: this.rowNumber,\n                },\n                ArrowLeft: {\n                    colNumber: (this.colNumber + (isRtl ? 1 : -1)) || 1,\n                    rowNumber: this.rowNumber,\n                },\n                ArrowUp: {\n                    colNumber: this.colNumber,\n                    rowNumber: this.rowNumber - 1 || 1,\n                },\n                ArrowDown: {\n                    colNumber: this.colNumber,\n                    rowNumber: this.rowNumber + 1,\n                },\n            };\n            const action = actions[e.key];\n            if (action) {\n                this.rowNumber = action.rowNumber || this.rowNumber;\n                this.colNumber = action.colNumber || this.colNumber;\n                this.render();\n\n                e.preventDefault();\n            } else if (e.key === 'Enter') {\n                this.selectCell();\n                e.preventDefault();\n            } else if (e.key === 'Escape') {\n                stop();\n                e.preventDefault();\n            }\n        };\n\n        const offset = getRangePosition(this.el, this.options.document, this.options);\n        if (isRtl) {\n            this.el.style.right = `${offset.right}px`;\n        } else {\n            this.el.style.left = `${offset.left}px`;\n        }\n\n        this.el.style.top = `${offset.top}px`;\n\n        const stop = () => {\n            this.hide();\n            this.options.document.removeEventListener('mousedown', stop);\n            this.removeEventListener('cell-selected', stop);\n            this.options.document.removeEventListener('keydown', keydown, true);\n        };\n\n        // Allow the mousedown that activate this command callback to release before adding the listener.\n        setTimeout(() => {\n            this.options.document.addEventListener('mousedown', stop);\n        });\n        this.options.document.addEventListener('keydown', keydown, true);\n        this.addEventListener('cell-selected', stop);\n    }\n}\n", "/** @odoo-module **/\n/**\n * program: \"patienceDiff\" algorithm implemented in javascript.\n * author: Jonathan Trent\n * version: 2.0\n *\n * use:  patienceDiff( aLines[], bLines[], diffPlusFlag)\n *\n * where:\n *      aLines[] contains the original text lines.\n *      bLines[] contains the new text lines.\n *      diffPlusFlag if true, returns additional arrays with the subset of lines that were\n *          either deleted or inserted.  These additional arrays are used by patienceDiffPlus.\n *\n * returns an object with the following properties:\n *      lines[] with properties of:\n *          line containing the line of text from aLines or bLines.\n *          aIndex referencing the index in aLine[].\n *          bIndex referencing the index in bLines[].\n *              (Note:  The line is text from either aLines or bLines, with aIndex and bIndex\n *               referencing the original index. If aIndex === -1 then the line is new from bLines,\n *               and if bIndex === -1 then the line is old from aLines.)\n *          moved is true if the line was moved from elsewhere in aLines[] or bLines[].\n *      lineCountDeleted is the number of lines from aLines[] not appearing in bLines[].\n *      lineCountInserted is the number of lines from bLines[] not appearing in aLines[].\n *      lineCountMoved is the number of lines moved outside of the Longest Common Subsequence.\n *\n */\n\nexport function patienceDiff(aLines, bLines, diffPlusFlag) {\n    //\n    // findUnique finds all unique values in arr[lo..hi], inclusive.  This\n    // function is used in preparation for determining the longest common\n    // subsequence.  Specifically, it first reduces the array range in question\n    // to unique values.\n    //\n    // Returns an ordered Map, with the arr[i] value as the Map key and the\n    // array index i as the Map value.\n    //\n    function findUnique(arr, lo, hi) {\n        var lineMap = new Map();\n\n        for (let i = lo; i <= hi; i++) {\n            let line = arr[i];\n            if (lineMap.has(line)) {\n                lineMap.get(line).count++;\n                lineMap.get(line).index = i;\n            } else {\n                lineMap.set(line, { count: 1, index: i });\n            }\n        }\n\n        lineMap.forEach((val, key, map) => {\n            if (val.count !== 1) {\n                map.delete(key);\n            } else {\n                map.set(key, val.index);\n            }\n        });\n\n        return lineMap;\n    }\n\n    //\n    // uniqueCommon finds all the unique common entries between aArray[aLo..aHi]\n    // and bArray[bLo..bHi], inclusive.  This function uses findUnique to pare\n    // down the aArray and bArray ranges first, before then walking the comparison\n    // between the two arrays.\n    //\n    // Returns an ordered Map, with the Map key as the common line between aArray\n    // and bArray, with the Map value as an object containing the array indexes of\n    // the matching unique lines.\n    //\n    function uniqueCommon(aArray, aLo, aHi, bArray, bLo, bHi) {\n        let ma = findUnique(aArray, aLo, aHi);\n        let mb = findUnique(bArray, bLo, bHi);\n\n        ma.forEach((val, key, map) => {\n            if (mb.has(key)) {\n                map.set(key, { indexA: val, indexB: mb.get(key) });\n            } else {\n                map.delete(key);\n            }\n        });\n\n        return ma;\n    }\n\n    //\n    // longestCommonSubsequence takes an ordered Map from the function uniqueCommon\n    // and determines the Longest Common Subsequence (LCS).\n    //\n    // Returns an ordered array of objects containing the array indexes of the\n    // matching lines for a LCS.\n    //\n    function longestCommonSubsequence(abMap) {\n        var ja = [];\n\n        // First, walk the list creating the jagged array.\n        abMap.forEach((val, key, map) => {\n            let i = 0;\n            while (ja[i] && ja[i][ja[i].length - 1].indexB < val.indexB) {\n                i++;\n            }\n\n            if (!ja[i]) {\n                ja[i] = [];\n            }\n\n            if (0 < i) {\n                val.prev = ja[i - 1][ja[i - 1].length - 1];\n            }\n\n            ja[i].push(val);\n        });\n\n        // Now, pull out the longest common subsequence.\n        var lcs = [];\n        if (0 < ja.length) {\n            let n = ja.length - 1;\n            var lcs = [ja[n][ja[n].length - 1]];\n            while (lcs[lcs.length - 1].prev) {\n                lcs.push(lcs[lcs.length - 1].prev);\n            }\n        }\n\n        return lcs.reverse();\n    }\n\n    // \"result\" is the array used to accumulate the aLines that are deleted, the\n    // lines that are shared between aLines and bLines, and the bLines that were\n    // inserted.\n    let result = [];\n    let deleted = 0;\n    let inserted = 0;\n\n    // aMove and bMove will contain the lines that don't match, and will be returned\n    // for possible searching of lines that moved.\n\n    let aMove = [];\n    let aMoveIndex = [];\n    let bMove = [];\n    let bMoveIndex = [];\n\n    //\n    // addToResult simply pushes the latest value onto the \"result\" array.  This\n    // array captures the diff of the line, aIndex, and bIndex from the aLines\n    // and bLines array.\n    //\n    function addToResult(aIndex, bIndex) {\n        if (bIndex < 0) {\n            aMove.push(aLines[aIndex]);\n            aMoveIndex.push(result.length);\n            deleted++;\n        } else if (aIndex < 0) {\n            bMove.push(bLines[bIndex]);\n            bMoveIndex.push(result.length);\n            inserted++;\n        }\n\n        result.push({\n            line: 0 <= aIndex ? aLines[aIndex] : bLines[bIndex],\n            aIndex: aIndex,\n            bIndex: bIndex,\n        });\n    }\n\n    //\n    // addSubMatch handles the lines between a pair of entries in the LCS.  Thus,\n    // this function might recursively call recurseLCS to further match the lines\n    // between aLines and bLines.\n    //\n    function addSubMatch(aLo, aHi, bLo, bHi) {\n        // Match any lines at the beginning of aLines and bLines.\n        while (aLo <= aHi && bLo <= bHi && aLines[aLo] === bLines[bLo]) {\n            addToResult(aLo++, bLo++);\n        }\n\n        // Match any lines at the end of aLines and bLines, but don't place them\n        // in the \"result\" array just yet, as the lines between these matches at\n        // the beginning and the end need to be analyzed first.\n        let aHiTemp = aHi;\n        while (aLo <= aHi && bLo <= bHi && aLines[aHi] === bLines[bHi]) {\n            aHi--;\n            bHi--;\n        }\n\n        // Now, check to determine with the remaining lines in the subsequence\n        // whether there are any unique common lines between aLines and bLines.\n        //\n        // If not, add the subsequence to the result (all aLines having been\n        // deleted, and all bLines having been inserted).\n        //\n        // If there are unique common lines between aLines and bLines, then let's\n        // recursively perform the patience diff on the subsequence.\n        let uniqueCommonMap = uniqueCommon(aLines, aLo, aHi, bLines, bLo, bHi);\n        if (uniqueCommonMap.size === 0) {\n            while (aLo <= aHi) {\n                addToResult(aLo++, -1);\n            }\n            while (bLo <= bHi) {\n                addToResult(-1, bLo++);\n            }\n        } else {\n            recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap);\n        }\n\n        // Finally, let's add the matches at the end to the result.\n        while (aHi < aHiTemp) {\n            addToResult(++aHi, ++bHi);\n        }\n    }\n\n    //\n    // recurseLCS finds the longest common subsequence (LCS) between the arrays\n    // aLines[aLo..aHi] and bLines[bLo..bHi] inclusive.  Then for each subsequence\n    // recursively performs another LCS search (via addSubMatch), until there are\n    // none found, at which point the subsequence is dumped to the result.\n    //\n    function recurseLCS(aLo, aHi, bLo, bHi, uniqueCommonMap) {\n        var x = longestCommonSubsequence(\n            uniqueCommonMap || uniqueCommon(aLines, aLo, aHi, bLines, bLo, bHi),\n        );\n        if (x.length === 0) {\n            addSubMatch(aLo, aHi, bLo, bHi);\n        } else {\n            if (aLo < x[0].indexA || bLo < x[0].indexB) {\n                addSubMatch(aLo, x[0].indexA - 1, bLo, x[0].indexB - 1);\n            }\n\n            let i;\n            for (i = 0; i < x.length - 1; i++) {\n                addSubMatch(x[i].indexA, x[i + 1].indexA - 1, x[i].indexB, x[i + 1].indexB - 1);\n            }\n\n            if (x[i].indexA <= aHi || x[i].indexB <= bHi) {\n                addSubMatch(x[i].indexA, aHi, x[i].indexB, bHi);\n            }\n        }\n    }\n\n    recurseLCS(0, aLines.length - 1, 0, bLines.length - 1);\n\n    if (diffPlusFlag) {\n        return {\n            lines: result,\n            lineCountDeleted: deleted,\n            lineCountInserted: inserted,\n            lineCountMoved: 0,\n            aMove: aMove,\n            aMoveIndex: aMoveIndex,\n            bMove: bMove,\n            bMoveIndex: bMoveIndex,\n        };\n    }\n\n    return {\n        lines: result,\n        lineCountDeleted: deleted,\n        lineCountInserted: inserted,\n        lineCountMoved: 0,\n    };\n}\n", "/** @odoo-module **/\nimport { patienceDiff } from './patienceDiff.js';\nimport { closestBlock, getRangePosition } from '../utils/utils.js';\n\nconst REGEX_RESERVED_CHARS = /[\\\\^$.*+?()[\\]{}|]/g;\n/**\n * Make `num` cycle from 0 to `max`.\n */\nfunction cycle(num, max) {\n    const y = max + 1;\n    return ((num % y) + y) % y;\n}\n\n/**\n * interface PowerboxCommand {\n *     category: string;\n *     name: string;\n *     priority: number;\n *     description: string;\n *     fontawesome: string; // a fontawesome class name\n *     callback: () => void; // to execute when the command is picked\n *     isDisabled?: () => boolean; // return true to disable the command\n * }\n */\n\nexport class Powerbox {\n    constructor({\n        categories, commands, commandFilters, editable, getContextFromParentRect,\n        onShow, onStop, beforeCommand, afterCommand\n    } = {}) {\n        this.categories = categories;\n        this.commands = commands;\n        this.commandFilters = commandFilters || [];\n        this.editable = editable;\n        this.getContextFromParentRect = getContextFromParentRect;\n        this.onShow = onShow;\n        this.onStop = onStop;\n        this.beforeCommand = beforeCommand;\n        this.afterCommand = afterCommand;\n        this.isOpen = false;\n        this.document = editable.ownerDocument;\n\n        // Draw the powerbox.\n        this.el = document.createElement('div');\n        this.el.className = 'oe-powerbox-wrapper position-absolute overflow-hidden';\n        this.el.style.display = 'none';\n        document.body.append(this.el);\n        this._mainWrapperElement = document.createElement('div');\n        this._mainWrapperElement.className = 'oe-powerbox-mainWrapper flex-skrink-1 overflow-auto py-2';\n        this.el.append(this._mainWrapperElement);\n        this.el.addEventListener('mousedown', ev => ev.stopPropagation());\n\n        // Set up events for later binding.\n        this._boundOnKeyup = this._onKeyup.bind(this);\n        this._boundOnKeydown = this._onKeydown.bind(this);\n        this._boundClose = this.close.bind(this);\n        this._events = [\n            [this.document, 'keyup', this._boundOnKeyup],\n            [this.document, 'keydown', this._boundOnKeydown, true],\n            [this.document, 'mousedown', this._boundClose],\n        ]\n        // If the global document is different from the provided\n        // options.document, which happens when the editor is inside an iframe,\n        // we need to listen to the mouse event on both documents to be sure the\n        // Powerbox will always close when clicking outside of it.\n        if (document !== this.document) {\n            this._events.push(\n                [document, 'mousedown', this._boundClose],\n            );\n        }\n\n    }\n    destroy() {\n        if (this.isOpen) {\n            this.close();\n        }\n        this.el.remove();\n    }\n\n    // -------------------------------------------------------------------------\n    // Public\n    // -------------------------------------------------------------------------\n\n    /**\n     * Open the Powerbox with the given commands or with all instance commands.\n     *\n     * @param {PowerboxCommand[]} [commands=this.commands]\n     * @param {Array<{name: string, priority: number}} [categories=this.categories]\n     */\n    open(commands=this.commands, categories=this.categories) {\n        commands = (commands || []).map(command => ({\n            ...command,\n            category: command.category || '',\n            name: command.name || '',\n            priority: command.priority || 0,\n            description: command.description || '',\n            callback: command.callback || (() => {}),\n        }));\n        categories = (categories || []).map(category => ({\n            name: category.name || '',\n            priority: category.priority || 0,\n        }));\n        const order = (a, b) => b.priority - a.priority || a.name.localeCompare(b.name);\n        // Remove duplicate category names, keeping only last declared version,\n        // and order them.\n        categories = [...categories].reverse().filter((category, index, cats) => (\n            cats.findIndex(cat => cat.name === category.name) === index\n        )).sort(order);\n\n        // Apply optional filters to disable commands, then order them.\n        for (let filter of this.commandFilters) {\n            commands = filter(commands);\n        }\n        commands = commands.filter(command => !command.isDisabled || !command.isDisabled()).sort(order);\n        commands = this._groupCommands(commands, categories).flatMap(group => group[1]);\n\n        const selection = this.document.getSelection();\n        const currentBlock = (selection && closestBlock(selection.anchorNode)) || this.editable;\n        this._context = {\n            commands, categories, filteredCommands: commands, selectedCommand: undefined,\n            initialTarget: currentBlock, initialValue: currentBlock.textContent,\n            lastText: undefined,\n        }\n        this.isOpen = true;\n        this._render(this._context.commands, this._context.categories);\n        this._bindEvents();\n        this.show();\n    }\n    /**\n     * Close the Powerbox without destroying it. Unbind events, reset context\n     * and call the optional `onStop` hook.\n     */\n    close() {\n        this.isOpen = false;\n        this.hide();\n        this._context = undefined;\n        this._unbindEvents();\n        this.onStop && this.onStop();\n    };\n    /**\n     * Show the Powerbox and position it. Call the optional `onShow` hook.\n     */\n    show() {\n        this.onShow && this.onShow();\n        this.el.style.display = 'flex';\n        this._resetPosition();\n    }\n    /**\n     * Hide the Powerbox. If the Powerbox is active, close it.\n     *\n     * @see close\n     */\n    hide() {\n        this.el.style.display = 'none';\n        if (this.isOpen) {\n            this.close();\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Private\n    // -------------------------------------------------------------------------\n\n    /**\n     * Render the Powerbox with the given commands, grouped by `category`.\n     *\n     * @private\n     * @param {PowerboxCommand[]} commands\n     * @param {Array<{name: string, priority: number}} categories\n     */\n    _render(commands, categories) {\n        const parser = new DOMParser();\n        this._mainWrapperElement.innerHTML = '';\n        this._hoverActive = false;\n        this._mainWrapperElement.classList.toggle('oe-powerbox-noResult', commands.length === 0);\n        this._context.selectedCommand = commands.find(command => command === this._context.selectedCommand) || commands[0];\n        for (const [category, categoryCommands] of this._groupCommands(commands, categories)) {\n            const categoryWrapperEl = parser.parseFromString(`\n                <div class=\"oe-powerbox-categoryWrapper\">\n                    <div class=\"oe-powerbox-category mx-3 my-1 text-uppercase\"></div>\n                </div>`, 'text/html').body.firstChild;\n            this._mainWrapperElement.append(categoryWrapperEl);\n            categoryWrapperEl.firstElementChild.innerText = category;\n            for (const command of categoryCommands) {\n                const commandElWrapper = document.createElement('div');\n                commandElWrapper.className = 'oe-powerbox-commandWrapper d-flex align-items-center px-3 py-2 cursor-pointer';\n                commandElWrapper.classList.toggle('active', this._context.selectedCommand === command);\n                commandElWrapper.replaceChildren(...parser.parseFromString(`\n                    <div class=\"oe-powerbox-commandLeftCol border rounded\">\n                        <i class=\"oe-powerbox-commandImg d-flex align-items-center justify-content-center fa\"></i>\n                    </div>\n                    <div class=\"oe-powerbox-commandRightCol ms-3\">\n                        <div class=\"oe-powerbox-commandName\"></div>\n                        <div class=\"oe-powerbox-commandDescription\"></div>\n                    </div>`, 'text/html').body.children);\n                commandElWrapper.querySelector('.oe-powerbox-commandImg').classList.add(command.fontawesome);\n                commandElWrapper.querySelector('.oe-powerbox-commandName').innerText = command.name;\n                commandElWrapper.querySelector('.oe-powerbox-commandDescription').innerText = command.description;\n                categoryWrapperEl.append(commandElWrapper);\n                // Handle events on command (activate and pick).\n                commandElWrapper.addEventListener('mouseenter', () => {\n                    this.el.querySelector('.oe-powerbox-commandWrapper.active').classList.remove('active');\n                    this._context.selectedCommand = command;\n                    commandElWrapper.classList.add('active');\n                });\n                commandElWrapper.addEventListener('click', ev => {\n                        ev.preventDefault();\n                        ev.stopImmediatePropagation();\n                        this._pickCommand(command);\n                    }, true,\n                );\n            }\n        }\n        // Hide category name if there is only a single one.\n        if (this._mainWrapperElement.childElementCount === 1) {\n            this._mainWrapperElement.querySelector('.oe-powerbox-category').style.display = 'none';\n        }\n        this._resetPosition();\n    }\n    /**\n     * Handle the selection of a command: call the command's callback. Also call\n     * the `beforeCommand` and `afterCommand` hooks if they exists.\n     *\n     * @private\n     * @param {PowerboxCommand} [command=this._context.selectedCommand]\n     */\n    async _pickCommand(command=this._context.selectedCommand) {\n        if (command) {\n            if (this.beforeCommand) {\n                await this.beforeCommand();\n            }\n            await command.callback();\n            if (this.afterCommand) {\n                await this.afterCommand();\n            }\n        }\n        this.close();\n    };\n    /**\n     * Takes a list of commands and returns an object whose keys are all\n     * existing category names and whose values are each of these categories'\n     * commands. Categories with no commands are removed.\n     *\n     * @private\n     * @param {PowerboxCommand[]} commands\n     * @param {Array<{name: string, priority: number}} categories\n     * @returns {{Array<[string, PowerboxCommand[]]>}>}\n     */\n    _groupCommands(commands, categories) {\n        const groups = [];\n        for (const category of categories) {\n            const categoryCommands = commands.filter(command => command.category === category.name);\n            commands = commands.filter(command => command.category !== category.name);\n            groups.push([category.name, categoryCommands]);\n        }\n        // If commands remain, it means they declared categories that didn't\n        // exist. Add these categories alphabetically at the end of the list.\n        const remainingCategories = [...new Set(commands.map(command => command.category))];\n        for (const categoryName of remainingCategories.sort((a, b) => a.localeCompare(b))) {\n            const categoryCommands = commands.filter(command => command.category === categoryName);\n            groups.push([categoryName, categoryCommands]);\n        }\n        return groups.filter(group => group[1].length);\n    }\n    /**\n     * Take an array of commands or categories and return a reordered copy of\n     * it, based on their respective priorities.\n     *\n     * @param {PowerboxCommand[] | Array<{name: string, priority: number}} commandsOrCategories\n     * @returns {PowerboxCommand[] | Array<{name: string, priority: number}}\n     */\n    _orderByPriority(commandsOrCategories) {\n        return [...commandsOrCategories].sort((a, b) => b.priority - a.priority || a.name.localeCompare(b.name));\n    }\n    /**\n     * Recompute the Powerbox's position base on the selection in the document.\n     *\n     * @private\n     */\n    _resetPosition() {\n        const position = getRangePosition(this.el, this.document, { getContextFromParentRect: this.getContextFromParentRect });\n        if (position) {\n            let { left, top } = position;\n            this.el.style.left = `${left}px`;\n            this.el.style.top = `${top}px`;\n        } else {\n            this.hide();\n        }\n    }\n    /**\n     * Add all events to their given target, based on @see _events.\n     *\n     * @private\n     */\n    _bindEvents() {\n        for (const [target, eventName, callback, option] of this._events) {\n            target.addEventListener(eventName, callback, option);\n        }\n    }\n    /**\n     * Remove all events from their given target, based on @see _events.\n     *\n     * @private\n     */\n    _unbindEvents() {\n        for (const [target, eventName, callback, option] of this._events) {\n            target.removeEventListener(eventName, callback, option);\n        }\n    }\n\n    // -------------------------------------------------------------------------\n    // Handlers\n    // -------------------------------------------------------------------------\n\n    /**\n     * Handle keyup events to filter commands based on what was typed, and\n     * prevent changing selection when using the arrow keys.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onKeyup(ev) {\n        if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {\n            ev.preventDefault();\n        } else {\n            const diff = patienceDiff(\n                this._context.initialValue.split(''),\n                this._context.initialTarget.textContent.split(''),\n                true,\n            );\n            this._context.lastText = diff.bMove.join('').replaceAll('\\ufeff', '');\n            const selection = this.document.getSelection();\n            if (\n                this._context.lastText.match(/\\s/) ||\n                !selection ||\n                this._context.initialTarget !== closestBlock(selection.anchorNode)\n            ) {\n                this.close();\n            } else {\n                const term = this._context.lastText.toLowerCase()\n                    .replaceAll(/\\s/g, '\\\\s')\n                    .replaceAll('\\u200B', '')\n                    .replace(REGEX_RESERVED_CHARS, '\\\\$&');\n                if (term.length) {\n                    const exactRegex = new RegExp(term, 'i');\n                    const fuzzyRegex = new RegExp(term.match(/\\\\.|./g).join('.*'), 'i');\n                    this._context.filteredCommands = this._context.commands.filter(command => {\n                        const commandText = (command.category + ' ' + command.name);\n                        const commandDescription = command.description.replace(/\\s/g, '');\n                        return commandText.match(fuzzyRegex) || commandDescription.match(exactRegex);\n                    });\n                } else {\n                    this._context.filteredCommands = this._context.commands;\n                }\n                this._render(this._context.filteredCommands, this._context.categories);\n            }\n        }\n    }\n    /**\n     * Handle keydown events to add keyboard interactions with the Powerbox.\n     *\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onKeydown(ev) {\n        if (['Enter', 'Tab'].includes(ev.key)) {\n            ev.stopImmediatePropagation();\n            this._pickCommand();\n            ev.preventDefault();\n        } else if (ev.key === 'Escape') {\n            ev.stopImmediatePropagation();\n            this.close();\n            ev.preventDefault();\n        } else if (ev.key === 'Backspace' && !this._context.lastText) {\n            this.close();\n        } else if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {\n            ev.preventDefault();\n            ev.stopImmediatePropagation();\n\n            const commandIndex = this._context.filteredCommands.findIndex(\n                command => command === this._context.selectedCommand,\n            );\n            if (this._context.filteredCommands.length && commandIndex !== -1) {\n                const nextIndex = commandIndex + (ev.key === 'ArrowDown' ? 1 : -1);\n                const newIndex = cycle(nextIndex, this._context.filteredCommands.length - 1);\n                this._context.selectedCommand = this._context.filteredCommands[newIndex];\n            } else {\n                this._context.selectedCommand = undefined;\n            }\n            this._render(this._context.filteredCommands, this._context.categories);\n            const activeCommand = this.el.querySelector('.oe-powerbox-commandWrapper.active');\n            if (activeCommand) {\n                activeCommand.scrollIntoView({block: 'nearest', inline: 'nearest'});\n            }\n        }\n    }\n}\n", "/** @odoo-module **/\nimport { childNodeIndex, isBlock } from '../utils/utils.js';\n\nText.prototype.oAlign = function (offset, mode) {\n    this.parentElement.oAlign(childNodeIndex(this), mode);\n};\n/**\n * This does not check for command state\n * @param {*} offset\n * @param {*} mode 'left', 'right', 'center' or 'justify'\n */\nHTMLElement.prototype.oAlign = function (offset, mode) {\n    if (!isBlock(this)) {\n        return this.parentElement.oAlign(childNodeIndex(this), mode);\n    }\n    const { textAlign } = getComputedStyle(this);\n    const alreadyAlignedLeft = textAlign === 'start' || textAlign === 'left';\n    const shouldApplyStyle = !(alreadyAlignedLeft && mode === 'left');\n    if (shouldApplyStyle) {\n        this.style.textAlign = mode;\n    }\n};\n", "/** @odoo-module **/\nimport { REGEX_BOOTSTRAP_COLUMN } from '../utils/constants.js';\nimport {\n    ancestors,\n    descendants,\n    childNodeIndex,\n    closestBlock,\n    closestElement,\n    DIRECTIONS,\n    getCursors,\n    getDeepRange,\n    getInSelection,\n    getListMode,\n    getSelectedNodes,\n    getTraversedNodes,\n    insertAndSelectZws,\n    isBlock,\n    isColorGradient,\n    isSelectionFormat,\n    isShrunkBlock,\n    isSelfClosingElement,\n    leftLeafFirstPath,\n    preserveCursor,\n    rightPos,\n    setSelection,\n    setCursorStart,\n    setTagName,\n    splitAroundUntil,\n    splitElement,\n    splitTextNode,\n    startPos,\n    nodeSize,\n    allowsParagraphRelatedElements,\n    isUnbreakable,\n    makeContentsInline,\n    unwrapContents,\n    getColumnIndex,\n    pxToFloat,\n    getRowIndex,\n    parseHTML,\n    formatSelection,\n    getDeepestPosition,\n    fillEmpty,\n    isEmptyBlock,\n    isWhitespace,\n    isVisibleTextNode,\n    getCursorDirection,\n    resetOuids,\n    FONT_SIZE_CLASSES,\n    TEXT_STYLE_CLASSES,\n    padLinkWithZws,\n    isLinkEligibleForZwnbsp,\n    paragraphRelatedElements,\n    lastLeaf,\n    firstLeaf,\n    convertList,\n} from '../utils/utils.js';\n\nconst TEXT_CLASSES_REGEX = /\\btext-[^\\s]*\\b/;\nconst BG_CLASSES_REGEX = /\\bbg-[^\\s]*\\b/;\n\nfunction align(editor, mode) {\n    const sel = editor.document.getSelection();\n    const visitedBlocks = new Set();\n    const traversedNode = getTraversedNodes(editor.editable);\n    for (const node of traversedNode) {\n        if (isVisibleTextNode(node)) {\n            const block = closestBlock(node);\n            if (!visitedBlocks.has(block)) {\n                const hasModifier = getComputedStyle(block).textAlign === mode;\n                if (!hasModifier && block.isContentEditable) {\n                    block.oAlign(sel.anchorOffset, mode);\n                }\n                visitedBlocks.add(block);\n            }\n        }\n    }\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 {string} mode 'color' or 'backgroundColor'\n */\nfunction 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/**\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 */\nfunction 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] && style[mode] !== 'inherit' && (!parent || style[mode] !== parent.style[mode])) ||\n        (classRegex.test(element.className) &&\n            (!parent || getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode]))\n    );\n}\n\n// This is a whitelist of the commands that are implemented by the\n// editor itself rather than the node prototypes. It might be\n// possible to switch the conditions and test if the method exist on\n// `sel.anchorNode` rather than relying on an expicit whitelist, but\n// the behavior would change if a method name exists both on the\n// editor and on the nodes. This is too risky to change in the\n// absence of a strong test suite, so the whitelist stays for now.\nexport const editorCommands = {\n    insert: (editor, content) => {\n        if (!content) return;\n        const selection = editor.document.getSelection();\n        let startNode;\n        let insertBefore = false;\n        if (!selection.isCollapsed) {\n            editor.deleteRange(selection);\n        }\n        const range = selection.getRangeAt(0);\n        const block = closestBlock(selection.anchorNode);\n        const isSelectionAtStart = firstLeaf(block) === selection.anchorNode && selection.anchorOffset === 0;\n        const isSelectionAtEnd = lastLeaf(block) === selection.focusNode && selection.focusOffset === nodeSize(selection.focusNode);\n        if (range.startContainer.nodeType === Node.TEXT_NODE) {\n            insertBefore = !range.startOffset;\n            splitTextNode(range.startContainer, range.startOffset, DIRECTIONS.LEFT);\n            startNode = range.startContainer;\n        }\n\n        const container = document.createElement('fake-element');\n        const containerFirstChild = document.createElement('fake-element-fc');\n        const containerLastChild = document.createElement('fake-element-lc');\n\n        if (typeof content === 'string') {\n            container.textContent = content;\n        } else {\n            container.replaceChildren(content);\n        }\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 isList = node => ['UL', 'OL'].includes(node.nodeName);\n        const hasSingleChild = container.childNodes.length === 1;\n        if (\n            closestElement(selection.anchorNode, 'UL, OL') &&\n            isList(container.firstChild)\n        ) {\n            unwrapContents(container.firstChild);\n        }\n        // Similarly if the html inserted ends with a list.\n        if (\n            closestElement(selection.focusNode, 'UL, OL') &&\n            isList(container.lastChild) &&\n            !hasSingleChild\n        ) {\n            unwrapContents(container.lastChild);\n        }\n\n        startNode = startNode || editor.document.getSelection().anchorNode;\n        const shouldUnwrap = (node) => (\n            [...paragraphRelatedElements, 'LI'].includes(node.nodeName) &&\n            block.textContent !== \"\" && node.textContent !== \"\" &&\n            [node.nodeName, 'DIV'].includes(block.nodeName) &&\n            // If the selection anchorNode is the editable itself, the content\n            // should not be unwrapped.\n            selection.anchorNode.oid !== 'root'\n        );\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>. The same is true for a <pre> inside a <pre>.\n        if (\n            container.childElementCount === 1 &&\n            (\n                ['P', 'LI'].includes(container.firstChild.nodeName) ||\n                shouldUnwrap(container.firstChild)\n            ) && selection.anchorNode.oid !== 'root'\n        ) {\n            const p = container.firstElementChild;\n            container.replaceChildren(...p.childNodes);\n        } else if (container.childElementCount > 1) {\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 (container.firstChild.nodeName === 'LI') {\n                    const deepestBlock = closestBlock(firstLeaf(container.firstChild));\n                    splitAroundUntil(deepestBlock, container.firstChild);\n                    container.firstElementChild.replaceChildren(...deepestBlock.childNodes);\n                }\n                containerFirstChild.replaceChildren(...container.firstElementChild.childNodes);\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 (container.lastChild.nodeName === 'LI') {\n                    const deepestBlock = closestBlock(lastLeaf(container.lastChild));\n                    splitAroundUntil(deepestBlock, container.lastChild);\n                    container.lastElementChild.replaceChildren(...deepestBlock.childNodes);\n                }\n                containerLastChild.replaceChildren(...container.lastElementChild.childNodes);\n                container.lastElementChild.remove();\n            }\n        }\n\n        if (startNode.nodeType === Node.ELEMENT_NODE) {\n            if (selection.anchorOffset === 0) {\n                const textNode = editor.document.createTextNode('');\n                if (isSelfClosingElement(startNode)) {\n                    startNode.parentNode.insertBefore(textNode, startNode);\n                } else {\n                    startNode.prepend(textNode);\n                }\n                startNode = textNode;\n            } else {\n                startNode = startNode.childNodes[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        let lastChildNode = false;\n        const currentList = currentNode && closestElement(currentNode, 'UL, OL');\n        const mode = currentList && getListMode(currentList);\n\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 = [...containerLastChild.childNodes];\n        if (containerLastChild.hasChildNodes()) {\n            const toInsert = [...containerLastChild.childNodes]; // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = insertBefore ? toInsert[0] : currentNode;\n            lastChildNode = toInsert[toInsert.length - 1];\n        }\n        const firstInsertedNodes = [...containerFirstChild.childNodes];\n        if (containerFirstChild.hasChildNodes()) {\n            const toInsert = [...containerFirstChild.childNodes]; // Prevent mutation\n            _insertAt(currentNode, [...toInsert], insertBefore);\n            currentNode = toInsert[toInsert.length - 1];\n            insertBefore = false;\n        }\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 (isUnbreakable(closestBlock(currentNode.nextSibling))) {\n                currentNode.nextSibling.oShiftEnter(0);\n            } else {\n                // If we arrive here, the o_enter index should always be 0.\n                const parent = currentNode.nextSibling.parentElement;\n                const index = [...parent.childNodes].indexOf(currentNode.nextSibling);\n                parent.oEnter(index);\n            }\n        }\n\n        let nodeToInsert;\n        const insertedNodes = [...container.childNodes];\n        while ((nodeToInsert = container.childNodes[0])) {\n            if (isBlock(nodeToInsert) && !allowsParagraphRelatedElements(currentNode)) {\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                    currentNode.parentElement !== editor.editable &&\n                    (!allowsParagraphRelatedElements(currentNode.parentElement) ||\n                        (currentNode.parentElement.nodeName === \"LI\" &&\n                            !isUnbreakable(nodeToInsert)))\n                ) {\n                    if (isUnbreakable(currentNode.parentElement)) {\n                        makeContentsInline(container);\n                        nodeToInsert = container.childNodes[0];\n                        break;\n                    }\n                    let offset = childNodeIndex(currentNode);\n                    if (!insertBefore) {\n                        offset += 1;\n                    }\n                    if (offset) {\n                        const [left, right] = splitElement(currentNode.parentElement, offset);\n                        if (isUnbreakable(nodeToInsert) && container.childNodes.length === 1) {\n                            fillEmpty(right);\n                        } else if (isEmptyBlock(right)) {\n                            right.remove();\n                        }\n                        currentNode = insertBefore ? right : left;\n                    } else {\n                        currentNode = currentNode.parentElement;\n                    }\n                }\n                if (currentNode.parentElement.nodeName === 'LI' && isUnbreakable(nodeToInsert)) {\n                    const br = document.createElement('br');\n                    currentNode[currentNode.textContent ? 'after' : 'before'](br);\n                }\n            }\n            // Ensure that all adjacent paragraph elements are converted to\n            // <li> when inserting in a list.\n            if (block.nodeName === \"LI\" && paragraphRelatedElements.includes(nodeToInsert.nodeName)) {\n                setTagName(nodeToInsert, \"LI\");\n            }\n            // Contenteditable false property changes to true after the node is\n            // inserted into DOM.\n            const isNodeToInsertContentEditable = nodeToInsert.isContentEditable;\n            if (insertBefore) {\n                currentNode.before(nodeToInsert);\n                insertBefore = false;\n            } else {\n                currentNode.after(nodeToInsert);\n            }\n            let convertedList;\n            if (\n                currentList &&\n                (\n                    (nodeToInsert.nodeName === 'LI' && nodeToInsert.classList.contains('oe-nested')) ||\n                    isList(nodeToInsert)\n                )\n            ) {\n                convertedList = convertList(nodeToInsert, mode);\n            }\n            if (currentNode.tagName !== 'BR' && isShrunkBlock(currentNode)) {\n                currentNode.remove();\n            }\n            // If the first child of editable is contenteditable false element\n            // a chromium bug prevents selecting the container. Prepend a\n            // zero-width space so it's no longer the first child.\n            if (\n                !isNodeToInsertContentEditable &&\n                editor.editable.firstChild === nodeToInsert &&\n                nodeToInsert.nodeType === Node.ELEMENT_NODE &&\n                (nodeToInsert.classList.contains(\"o_knowledge_behavior_type_template\") ||\n                    nodeToInsert.classList.contains(\"o_editor_banner\"))\n            ) {\n                const zws = document.createTextNode(\"\\u200B\");\n                nodeToInsert.before(zws);\n            }\n            currentNode = convertedList || nodeToInsert;\n        }\n\n        currentNode = lastChildNode || currentNode;\n        if (\n            !isUnbreakable(currentNode) &&\n            currentNode.nodeName !== 'BR' &&\n            currentNode.nextSibling &&\n            currentNode.nextSibling.nodeName === 'BR' &&\n            lastLeaf(currentNode.parentNode) === currentNode.nextSibling &&\n            !closestElement(currentNode, '[t-field],[t-esc],[t-out]')\n        ) {\n            currentNode.nextSibling.remove();\n        }\n        selection.removeAllRanges();\n        const newRange = new Range();\n        let lastPosition;\n        if (currentNode.nodeName === 'A' && isLinkEligibleForZwnbsp(editor.editable, currentNode)) {\n            padLinkWithZws(editor.editable, currentNode);\n            currentNode = currentNode.nextSibling;\n            lastPosition = getDeepestPosition(...rightPos(currentNode));\n        } else {\n            lastPosition = [...paragraphRelatedElements, 'LI', 'UL', 'OL'].includes(currentNode.nodeName)\n                ? rightPos(lastLeaf(currentNode))\n                : rightPos(currentNode);\n        }\n        if (\n            lastPosition[0].nodeName === \"A\" &&\n            (lastPosition[1] === nodeSize(lastPosition[0]) || lastPosition[1] === 0) &&\n            isLinkEligibleForZwnbsp(editor.editable, lastPosition[0])\n        ) {\n            // In case the currentNode is different than A but the lastposition is A\n            // we need to pad the link with zws and adjust the selection accordingly\n            padLinkWithZws(editor.editable, lastPosition[0]);\n            currentNode = lastPosition[0].nextSibling;\n            lastPosition = getDeepestPosition(...rightPos(currentNode));\n        }\n        if (!editor.options.allowInlineAtRoot && lastPosition[0] === editor.editable) {\n            // Correct the position if it happens to be in the editable root.\n            lastPosition = getDeepestPosition(...lastPosition);\n        }\n        newRange.setStart(lastPosition[0], lastPosition[1]);\n        newRange.setEnd(lastPosition[0], lastPosition[1]);\n        selection.addRange(newRange);\n        return [...firstInsertedNodes, ...insertedNodes, ...lastInsertedNodes];\n    },\n    insertFontAwesome: (editor, faClass = 'fa fa-star') => {\n        const insertedNode = editorCommands.insert(editor, document.createElement('i'))[0];\n        insertedNode.className = faClass;\n        const position = rightPos(insertedNode);\n        setSelection(...position, ...position, false);\n    },\n\n    // History\n    undo: editor => editor.historyUndo(),\n    redo: editor => editor.historyRedo(),\n\n    // Change tags\n    setTag(editor, tagName, extraClass = \"\") {\n        const range = getDeepRange(editor.editable, { correctTripleClick: true });\n        const selectedBlocks = [...new Set(getTraversedNodes(editor.editable, range).map(closestBlock))];\n        const deepestSelectedBlocks = selectedBlocks.filter(block => (\n            !descendants(block).some(descendant => selectedBlocks.includes(descendant)) &&\n            block.isContentEditable\n        ));\n        let { startContainer, startOffset, endContainer, endOffset } = range;\n        const startContainerChild = startContainer.firstChild;\n        const endContainerChild = endContainer.lastChild;\n        for (const block of deepestSelectedBlocks) {\n            if (\n                ['P', 'PRE', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'BLOCKQUOTE'].includes(\n                    block.nodeName,\n                )\n            ) {\n                const inLI = block.closest('li');\n                if (inLI && tagName === \"P\") {\n                    inLI.oToggleList(0);\n                } else {\n                    const newEl = setTagName(block, tagName);\n                    newEl.classList.remove(\n                        ...FONT_SIZE_CLASSES,\n                        ...TEXT_STYLE_CLASSES,\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                        \"h1\", \"h2\", \"h3\", \"h4\", \"h5\", \"h6\"\n                    );\n                    delete newEl.style.fontSize;\n                    if (extraClass) {\n                        newEl.classList.add(extraClass);\n                    }\n                    if (newEl.classList.length === 0) {\n                        newEl.removeAttribute(\"class\");\n                    }\n                }\n            } else {\n                // eg do not change a <div> into a h1: insert the h1\n                // into it instead.\n                const newBlock = editor.document.createElement(tagName);\n                const children = [...block.childNodes];\n                block.insertBefore(newBlock, block.firstChild);\n                children.forEach(child => newBlock.appendChild(child));\n            }\n        }\n        const isContextBlock = container => ['TD', 'DIV', 'LI'].includes(container.nodeName);\n        if (!startContainer.isConnected || isContextBlock(startContainer)) {\n            startContainer = startContainerChild?.parentNode || startContainer;\n        }\n        if (!endContainer.isConnected || isContextBlock(endContainer)) {\n            endContainer = endContainerChild?.parentNode || endContainer;\n        }\n        const newRange = new Range();\n        newRange.setStart(startContainer, startOffset);\n        newRange.setEnd(endContainer, endOffset);\n        getDeepRange(editor.editable, { range: newRange, select: true });\n        editor.historyStep();\n    },\n\n    // Formats\n    // -------------------------------------------------------------------------\n    bold: editor => formatSelection(editor, 'bold'),\n    italic: editor => formatSelection(editor, 'italic'),\n    underline: editor => formatSelection(editor, 'underline'),\n    strikeThrough: editor => formatSelection(editor, 'strikeThrough'),\n    setFontSize: (editor, size) => formatSelection(editor, 'fontSize', {applyStyle: true, formatProps: {size}}),\n    setFontSizeClassName: (editor, className) => formatSelection(editor, 'setFontSizeClassName', {formatProps: {className}}),\n    switchDirection: editor => {\n        getDeepRange(editor.editable, { splitText: true, select: true, correctTripleClick: true });\n        const selection = editor.document.getSelection();\n        const selectedTextNodes = [selection.anchorNode, ...getSelectedNodes(editor.editable), selection.focusNode]\n            .filter(n => n.nodeType === Node.TEXT_NODE && closestElement(n).isContentEditable && n.nodeValue.trim().length);\n\n        const changedElements = [];\n        const defaultDirection = editor.options.direction;\n        const shouldApplyStyle = !isSelectionFormat(editor.editable, 'switchDirection');\n        let blocks = new Set(selectedTextNodes.map(textNode => closestElement(textNode, 'ul,ol') || closestBlock(textNode)));\n        blocks.forEach(block => {\n            blocks = [...blocks, ...block.querySelectorAll('ul,ol')];\n        })\n        for (const block of blocks) {\n            if (!shouldApplyStyle) {\n                block.removeAttribute('dir');\n            } else {\n                block.setAttribute('dir', defaultDirection === 'ltr' ? 'rtl' : 'ltr');\n            }\n            changedElements.push(block);\n        }\n\n        for (const element of changedElements) {\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    },\n    removeFormat: editor => {\n        const textAlignStyles = new Map();\n        getTraversedNodes(editor.editable).forEach((element) => {\n            const block = closestBlock(element);\n            if (block.style.textAlign) {\n                textAlignStyles.set(block, block.style.textAlign);\n            }\n        });\n        // Calling `document.execCommand` will cause an input event with the\n        // input type \"formatRemove\". This would cause a new history step to be\n        // created in the middle of the process, which we prevent here.\n        editor.historyPauseSteps();\n        editor.document.execCommand('removeFormat');\n        for (const node of getTraversedNodes(editor.editable)) {\n            if (node.nodeType === Node.ELEMENT_NODE && node.hasAttribute('color')) {\n                node.removeAttribute('color');\n            }\n            const element = closestElement(node);\n            element.style.removeProperty('color');\n            element.style.removeProperty('background');\n        }\n        textAlignStyles.forEach((textAlign, block) => {\n            block.style.setProperty('text-align', textAlign);\n        });\n        editor.historyUnpauseSteps();\n    },\n\n    // Align\n    justifyLeft: editor => align(editor, 'left'),\n    justifyRight: editor => align(editor, 'right'),\n    justifyCenter: editor => align(editor, 'center'),\n    justifyFull: editor => align(editor, 'justify'),\n\n    // Link\n    unlink: editor => {\n        const sel = editor.document.getSelection();\n        const isCollapsed = sel.isCollapsed;\n        // If the selection is collapsed, unlink the whole link:\n        // `<a>a[]b</a>` => `a[]b`.\n        getDeepRange(editor.editable, { sel, splitText: true, select: true });\n        if (!isCollapsed) {\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 } = sel;\n            const direction = getCursorDirection(anchorNode, anchorOffset, focusNode, focusOffset);\n            // Split the links around the selection.\n            const [startLink, endLink] = [closestElement(anchorNode, 'a'), closestElement(focusNode, 'a')];\n            if (startLink) {\n                anchorNode = splitAroundUntil(anchorNode, startLink);\n                anchorOffset = direction === DIRECTIONS.RIGHT ? 0 : nodeSize(anchorNode);\n                setSelection(anchorNode, anchorOffset, focusNode, focusOffset, true);\n            }\n            // Only split the end link if it was not already done above.\n            if (endLink && endLink.isConnected) {\n                focusNode = splitAroundUntil(focusNode, endLink);\n                focusOffset = direction === DIRECTIONS.RIGHT ? nodeSize(focusNode) : 0;\n                setSelection(anchorNode, anchorOffset, focusNode, focusOffset, true);\n            }\n        }\n        const targetedNodes = isCollapsed ? [sel.anchorNode] : getSelectedNodes(editor.editable);\n        const links = new Set(targetedNodes.map(node => closestElement(node, 'a')).filter(a => a && a.isContentEditable));\n        if (links.size) {\n            const cr = preserveCursor(editor.document);\n            for (const link of links) {\n                unwrapContents(link);\n            }\n            cr();\n        }\n    },\n\n    // List\n    indentList: (editor, mode = 'indent') => {\n        const [pos1, pos2] = getCursors(editor.document);\n        const end = leftLeafFirstPath(...pos1).next().value;\n        const li = new Set();\n        for (const node of leftLeafFirstPath(...pos2)) {\n            const cli = closestElement(node,'li');\n            if (\n                cli &&\n                cli.tagName == 'LI' &&\n                !li.has(cli) &&\n                !cli.classList.contains('oe-nested') &&\n                cli.isContentEditable &&\n                !cli.classList.contains('nav-item')\n            ) {\n                li.add(cli);\n            }\n            if (node == end) break;\n        }\n        for (const node of li) {\n            if (mode == 'indent') {\n                node.oTab(0);\n            } else {\n                node.oShiftTab(0);\n            }\n        }\n        return true;\n    },\n    toggleList: (editor, mode) => {\n        const li = new Set();\n        const blocks = new Set();\n\n        const selectedBlocks = getTraversedNodes(editor.editable);\n        const deepestSelectedBlocks = selectedBlocks.filter(block => (\n            !descendants(block).some(descendant => selectedBlocks.includes(descendant))\n        ));\n        for (const node of deepestSelectedBlocks) {\n            let nodeToToggle = closestBlock(node);\n            if (\n                ![...paragraphRelatedElements, 'LI'].includes(nodeToToggle.nodeName) &&\n                node.nodeType === Node.TEXT_NODE && isWhitespace(node) && closestElement(node).isContentEditable\n            ) {\n                node.remove();\n            } else {\n                // Ensure nav-item lists are excluded from toggling\n                const isNavItemList = node => node.nodeName === 'LI' && node.classList.contains('nav-item');\n                nodeToToggle = isNavItemList(nodeToToggle) ? node : nodeToToggle;\n                if (!['OL', 'UL'].includes(nodeToToggle.tagName) && (nodeToToggle.isContentEditable || nodeToToggle.nodeType === Node.TEXT_NODE)) {\n                    const closestLi = closestElement(nodeToToggle, 'li');\n                    nodeToToggle = closestLi && !isNavItemList(closestLi) ? closestLi : nodeToToggle;\n                    const ublock = nodeToToggle.nodeName === 'LI' && nodeToToggle.closest('ol, ul');\n                    ublock && getListMode(ublock) == mode ? li.add(nodeToToggle) : blocks.add(nodeToToggle);\n                }\n            }\n        }\n\n        let target = [...(blocks.size ? blocks : li)];\n        if (blocks.size) {\n            // Remove hardcoded padding to have default padding of list element\n            for (const block of blocks) {\n                if (block.style) {\n                    block.style.padding = \"\";\n                }\n            }\n        }\n        while (target.length) {\n            const node = target.pop();\n            // only apply one li per ul\n            if (!node.oToggleList(0, mode)) {\n                target = target.filter(\n                    li => li.parentNode != node.parentNode || li.tagName != 'LI',\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 {Element} [element]\n     */\n    applyColor: (editor, color, mode, element) => {\n        const selectedTds = [...editor.editable.querySelectorAll('td.o_selected_td')].filter(\n            node => closestElement(node).isContentEditable\n        );\n        let coloredTds = [];\n        if (selectedTds.length && mode === \"backgroundColor\") {\n            for (const td of selectedTds) {\n                colorElement(td, color, mode);\n            }\n            coloredTds = [...selectedTds];\n        } else if (element) {\n            colorElement(element, color, mode);\n            return [element];\n        }\n        const selection = editor.document.getSelection();\n        let wasCollapsed = false;\n        if (selection.getRangeAt(0).collapsed && !selectedTds.length) {\n            insertAndSelectZws(selection);\n            wasCollapsed = true;\n        }\n        const range = getDeepRange(editor.editable, { splitText: true, select: true });\n        if (!range) return;\n        const restoreCursor = preserveCursor(editor.document);\n        // Get the <font> nodes to color\n        const selectionNodes = getSelectedNodes(editor.editable).filter(node => closestElement(node).isContentEditable);\n        if (isEmptyBlock(range.endContainer)) {\n            selectionNodes.push(range.endContainer, ...descendants(range.endContainer));\n        }\n        const selectedNodes = mode === \"backgroundColor\"\n            ? selectionNodes.filter(node => !closestElement(node, 'table.o_selected_table'))\n            : selectionNodes;\n        const selectedFieldNodes = new Set(getSelectedNodes(editor.editable)\n                .map(n => closestElement(n, \"*[t-field],*[t-out],*[t-esc]\"))\n                .filter(Boolean));\n\n        function getFonts(selectedNodes) {\n            return selectedNodes.flatMap(node => {\n                let font = closestElement(node, 'font') || closestElement(node, 'span');\n                const children = font && descendants(font);\n                if (font && (font.nodeName === 'FONT' || (font.nodeName === 'SPAN' && font.style[mode]))) {\n                    // Partially selected <font>: split it.\n                    const selectedChildren = children.filter(child => selectedNodes.includes(child));\n                    if (selectedChildren.length) {\n                        font = splitAroundUntil(selectedChildren, font);\n                    } else {\n                        font = [];\n                    }\n                } else if ((node.nodeType === Node.TEXT_NODE && !isWhitespace(node) && node.textContent !== '\\ufeff')\n                        || (node.nodeName === 'BR' && isEmptyBlock(node.parentNode))\n                        || (node.nodeType === Node.ELEMENT_NODE &&\n                            node.nodeName !== 'FIGURE' &&\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                    // 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 = 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            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            colorElement(font, color, mode);\n            if ((!hasColor(font, 'color') && !hasColor(font,'backgroundColor')) && (!font.hasAttribute('style') || !color)) {\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        restoreCursor();\n        if (wasCollapsed) {\n            const newSelection = editor.document.getSelection();\n            const range = new Range();\n            range.setStart(newSelection.anchorNode, newSelection.anchorOffset);\n            range.collapse(true);\n            newSelection.removeAllRanges();\n            newSelection.addRange(range);\n        }\n        return [...fontsSet, ...coloredTds];\n    },\n    // Table\n    insertTable: (editor, { rowNumber = 2, colNumber = 2 } = {}) => {\n        const tdsHtml = new Array(colNumber).fill('<td><p><br></p></td>').join('');\n        const trsHtml = new Array(rowNumber).fill(`<tr>${tdsHtml}</tr>`).join('');\n        const tableHtml = `<table class=\"table table-bordered o_table\"><tbody>${trsHtml}</tbody></table>`;\n        const sel = editor.document.getSelection();\n        if (!sel.isCollapsed) {\n            editor.deleteRange(sel);\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                : splitElement(anchorNode, sel.anchorOffset).shift();\n            const newPosition = rightPos(newAnchorNode);\n            setSelection(...newPosition, ...newPosition, false);\n        }\n        const [table] = editorCommands.insert(editor, parseHTML(editor.document, tableHtml));\n        setCursorStart(table.querySelector('p'));\n    },\n    addColumn: (editor, beforeOrAfter, referenceCell) => {\n        if (!referenceCell) {\n            getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td.\n            referenceCell = getInSelection(editor.document, 'td');\n            if (!referenceCell) return;\n        }\n        const columnIndex = getColumnIndex(referenceCell);\n        const table = closestElement(referenceCell, 'table');\n        const tableWidth = table.style.width ? pxToFloat(table.style.width) : table.clientWidth;\n        const referenceColumn = table.querySelectorAll(`tr td:nth-of-type(${columnIndex + 1})`);\n        const referenceCellWidth = referenceCell.style.width ? pxToFloat(referenceCell.style.width) : referenceCell.clientWidth;\n        // Temporarily set widths so proportions are respected.\n        const firstRow = table.querySelector('tr');\n        const firstRowCells = [...firstRow.children].filter(child => child.nodeName === 'TD' || child.nodeName === 'TH');\n        let totalWidth = 0;\n        for (const cell of firstRowCells) {\n            const width = cell.style.width ? pxToFloat(cell.style.width) : cell.clientWidth;\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(Math.round((width * tableWidth) / (tableWidth + referenceCellWidth - 1)), 13);\n            cell.style.width = newWidth + 'px';\n            totalWidth += newWidth;\n        }\n        referenceColumn.forEach((cell, rowIndex) => {\n            const newCell = document.createElement('td');\n            const p = document.createElement('p');\n            p.append(document.createElement('br'));\n            newCell.append(p);\n            cell[beforeOrAfter](newCell);\n            if (rowIndex === 0) {\n                newCell.style.width = cell.style.width;\n                totalWidth += pxToFloat(cell.style.width);\n            }\n        });\n        if (totalWidth !== tableWidth - 1) { // -1 for the width of the border of the new column.\n            firstRowCells[firstRowCells.length - 1].style.width = pxToFloat(firstRowCells[firstRowCells.length - 1].style.width) + (tableWidth - totalWidth - 1) + 'px';\n        }\n        // Fix the table and row's width so it doesn't change.\n        table.style.width = tableWidth + 'px';\n    },\n    addRow: (editor, beforeOrAfter, referenceRow) => {\n        if (!referenceRow) {\n            getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr.\n            referenceRow = getInSelection(editor.document, 'tr');\n            if (!referenceRow) return;\n        }\n        const referenceRowHeight = referenceRow.style.height ? pxToFloat(referenceRow.style.height) : referenceRow.clientHeight;\n        const newRow = document.createElement('tr');\n        newRow.style.height = referenceRowHeight + 'px';\n        const cells = referenceRow.querySelectorAll('td');\n        newRow.append(...Array.from(Array(cells.length)).map(() => {\n            const td = document.createElement('td');\n            const p = document.createElement('p');\n            p.append(document.createElement('br'));\n            td.append(p);\n            return td;\n        }));\n        referenceRow[beforeOrAfter](newRow);\n        newRow.style.height = referenceRowHeight + 'px';\n        if (getRowIndex(newRow) === 0) {\n            let columnIndex = 0;\n            for (const newColumn of newRow.children) {\n                newColumn.style.width = cells[columnIndex].style.width;\n                cells[columnIndex].style.width = '';\n                columnIndex++;\n            }\n        }\n    },\n    removeColumn: (editor, cell) => {\n        if (!cell) {\n            getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td.\n            cell = getInSelection(editor.document, 'td');\n            if (!cell) return;\n        }\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        if (table.style.width) {\n            const tableRect = table.getBoundingClientRect();\n            const cellRect = cell.getBoundingClientRect();\n            table.style.width = tableRect.width - cellRect.width + 'px';\n        }\n        table.querySelectorAll(`tr td:nth-of-type(${index + 1})`).forEach(td => td.remove());\n        siblingCell ? setSelection(...startPos(siblingCell)) : editorCommands.deleteTable(editor, table);\n    },\n    removeRow: (editor, row) => {\n        if (!row) {\n            getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr.\n            row = getInSelection(editor.document, 'tr');\n            if (!row) return;\n        }\n        const table = closestElement(row, 'table');\n        const rows = [...table.querySelectorAll('tr')];\n        const rowIndex = rows.findIndex(tr => tr === row);\n        const siblingRow = rows[rowIndex - 1] || rows[rowIndex + 1];\n        row.remove();\n        siblingRow ? setSelection(...startPos(siblingRow)) : editorCommands.deleteTable(editor, table);\n    },\n    resetSize: (editor,table) => {\n        if (!table) {\n            getDeepRange(editor.editable, { select: true });\n            table = getInSelection(editor.document,'table');\n        }\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: (editor, table) => {\n        table = table || getInSelection(editor.document, 'table');\n        if (!table) return;\n        const p = document.createElement('p');\n        p.appendChild(document.createElement('br'));\n        table.before(p);\n        table.remove();\n        setSelection(p, 0);\n    },\n    // Structure\n    columnize: (editor, numberOfColumns, addParagraphAfter=true) => {\n        const sel = editor.document.getSelection();\n        const anchor = sel.anchorNode;\n        const hasColumns = !!closestElement(anchor, '.o_text_columns');\n        if (!numberOfColumns && hasColumns) {\n            // Remove columns.\n            const restore = preserveCursor(editor.document);\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                    const columnContents = unwrapContents(column);\n                    for (const node of columnContents) {\n                        resetOuids(node);\n                    }\n                }\n            }\n            restore();\n        } else if (numberOfColumns && !hasColumns) {\n            // Create columns.\n            const restore = preserveCursor(editor.document);\n            const container = document.createElement('div');\n            if (!closestElement(anchor, '.container')) {\n                container.classList.add('container');\n            }\n            container.classList.add('o_text_columns');\n            const row = 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 = 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 p = document.createElement('p');\n                p.append(document.createElement('br'));\n                p.classList.add('oe-hint');\n                p.setAttribute('placeholder', 'New column...');\n                column.append(p);\n            }\n            restore();\n            if (addParagraphAfter) {\n                const p = document.createElement('p');\n                p.append(document.createElement('br'));\n                container.after(p);\n            }\n        } else if (numberOfColumns && hasColumns) {\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 > 0) {\n                // Add extra columns.\n                const restore = preserveCursor(editor.document);\n                for (const column of columns) {\n                    column.className = column.className.replace(REGEX_BOOTSTRAP_COLUMN, `col$1-${columnSize}`);\n                }\n                let lastColumn = columns[columns.length - 1];\n                for (let i = 0; i < diff; i++) {\n                    const column = document.createElement('div');\n                    column.classList.add(`col-${columnSize}`);\n                    const p = document.createElement('p');\n                    p.append(document.createElement('br'));\n                    p.classList.add('oe-hint');\n                    p.setAttribute('placeholder', 'New column...');\n                    column.append(p);\n                    lastColumn.after(column);\n                    lastColumn = column;\n                }\n                restore();\n            } else if (diff < 0) {\n                // Remove superfluous columns.\n                const restore = preserveCursor(editor.document);\n                for (const column of columns) {\n                    column.className = column.className.replace(REGEX_BOOTSTRAP_COLUMN, `col$1-${columnSize}`);\n                }\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                restore();\n            }\n        }\n    },\n    insertHorizontalRule(editor) {\n        const selection = editor.document.getSelection();\n        const range = selection.getRangeAt(0);\n        const element = closestElement(range.startContainer, paragraphRelatedElements) || closestBlock(range.startContainer);\n\n        if (element && ancestors(element).includes(editor.editable)) {\n            element.before(editor.document.createElement('hr'));\n        }\n    },\n};\n", "/** @odoo-module **/\nimport { UNBREAKABLE_ROLLBACK_CODE, UNREMOVABLE_ROLLBACK_CODE, REGEX_BOOTSTRAP_COLUMN } from '../utils/constants.js';\nimport {deleteText} from './deleteForward.js';\nimport {\n    boundariesOut,\n    childNodeIndex,\n    CTGROUPS,\n    CTYPES,\n    DIRECTIONS,\n    endPos,\n    fillEmpty,\n    getState,\n    isBlock,\n    isEmptyBlock,\n    isUnbreakable,\n    isUnremovable,\n    isVisible,\n    leftPos,\n    rightPos,\n    moveNodes,\n    nodeSize,\n    paragraphRelatedElements,\n    prepareUpdate,\n    setSelection,\n    isMediaElement,\n    isSelfClosingElement,\n    isNotEditableNode,\n    createDOMPathGenerator,\n    closestElement,\n    closestBlock,\n    getOffsetAndCharSize,\n    ZERO_WIDTH_CHARS,\n} from '../utils/utils.js';\n\nText.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {\n    const parentElement = this.parentElement;\n\n    if (!offset) {\n        // Backspace at the beginning of a text node is not a specific case to\n        // handle, let the element implementation handle it.\n        parentElement.oDeleteBackward([...parentElement.childNodes].indexOf(this), alreadyMoved);\n        return;\n    }\n    // Get the size of the unicode character to remove.\n    // If the current offset split an emoji in the middle , we need to change offset to the end of the emoji\n    const [newOffset, charSize] = getOffsetAndCharSize(this.nodeValue, offset, DIRECTIONS.LEFT);\n    deleteText.call(this, charSize, newOffset - charSize, DIRECTIONS.LEFT, alreadyMoved);\n};\n\nconst isDeletable = (node) => {\n    return isMediaElement(node) || isNotEditableNode(node);\n}\n\nHTMLElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false, offsetLimit) {\n    const contentIsZWS = ZERO_WIDTH_CHARS.includes(this.textContent);\n    let moveDest;\n    if (offset) {\n        const leftNode = this.childNodes[offset - 1];\n        if (isUnremovable(leftNode)) {\n            throw UNREMOVABLE_ROLLBACK_CODE;\n        }\n        if (\n            isDeletable(leftNode)\n        ) {\n            leftNode.remove();\n            return;\n        }\n        if (!isBlock(leftNode) || isSelfClosingElement(leftNode)) {\n            /**\n             * Backspace just after an inline node, convert to backspace at the\n             * end of that inline node.\n             *\n             * E.g. <p>abc<i>def</i>[]</p> + BACKSPACE\n             * <=>  <p>abc<i>def[]</i></p> + BACKSPACE\n             */\n            leftNode.oDeleteBackward(nodeSize(leftNode), alreadyMoved);\n            return;\n        }\n\n        /**\n         * Backspace just after an block node, we have to move any inline\n         * content after it, up to the next block. If the cursor is between\n         * two blocks, this is a theoretical case: just do nothing.\n         *\n         * E.g. <p>abc</p>[]de<i>f</i><p>ghi</p> + BACKSPACE\n         * <=>  <p>abcde<i>f</i></p><p>ghi</p>\n         */\n        alreadyMoved = true;\n        moveDest = endPos(leftNode);\n    } else {\n        if (isUnremovable(this)) {\n            throw UNREMOVABLE_ROLLBACK_CODE;\n        }\n        // Empty unbreakable blocks should be removed with backspace, with the\n        // notable exception of Bootstrap columns.\n        if (isUnbreakable(this) && (REGEX_BOOTSTRAP_COLUMN.test(this.className) || !isEmptyBlock(this))) {\n            throw UNBREAKABLE_ROLLBACK_CODE;\n        }\n        const parentEl = this.parentElement;\n        // Handle editable sub-nodes\n        if (\n            parentEl &&\n            parentEl.getAttribute(\"contenteditable\") === \"true\" &&\n            parentEl.oid !== \"root\" &&\n            parentEl.parentElement &&\n            !parentEl.parentElement.isContentEditable &&\n            paragraphRelatedElements.includes(this.tagName) &&\n            !this.previousElementSibling\n        ) {\n            // The first child element of a contenteditable=\"true\" zone which\n            // itself is contained in a contenteditable=\"false\" zone can not be\n            // removed if it is paragraph-like.\n            throw UNREMOVABLE_ROLLBACK_CODE;\n        }\n        const closestLi = closestElement(this, 'li');\n        if ((closestLi && !closestLi.previousElementSibling) || !isBlock(this) || isSelfClosingElement(this)) {\n            /**\n             * Backspace at the beginning of an inline node, nothing has to be\n             * done: propagate the backspace. If the node was empty, we remove\n             * it before.\n             *\n             * E.g. <p>abc<b></b><i>[]def</i></p> + BACKSPACE\n             * <=>  <p>abc<b>[]</b><i>def</i></p> + BACKSPACE\n             * <=>  <p>abc[]<i>def</i></p> + BACKSPACE\n             */\n            const parentOffset = childNodeIndex(this);\n\n            if (!nodeSize(this) || contentIsZWS) {\n                const visible = isVisible(this);\n                const restore = prepareUpdate(...boundariesOut(this));\n                this.remove();\n                restore();\n\n                fillEmpty(parentEl);\n\n                if (visible) {\n                    // TODO this handle BR/IMG/etc removals../ to see if we\n                    // prefer to have a dedicated handler for every possible\n                    // HTML element or if we let this generic code handle it.\n                    setSelection(parentEl, parentOffset);\n                    return;\n                }\n            }\n            parentEl.oDeleteBackward(parentOffset, alreadyMoved);\n            return;\n        }\n\n        /** If we are at the beninning of a block node,\n         *  And the previous node is empty, remove it.\n         *\n         *   E.g. (previousEl == empty)\n         *        <p><br></p><h1>[]def</h1> + BACKSPACE\n         *   <=>  <h1>[]def</h1>\n         *\n         *   E.g. (previousEl != empty)\n         *        <h3>abc</h3><h1>[]def</h1> + BACKSPACE\n         *   <=>  <h3>abc[]def</h3>\n        */\n        const previousElementSiblingClosestBlock = closestBlock(this.previousElementSibling);\n        if (\n            previousElementSiblingClosestBlock &&\n            (isEmptyBlock(previousElementSiblingClosestBlock) ||\n                previousElementSiblingClosestBlock.textContent === '\\u200B') &&\n            paragraphRelatedElements.includes(this.nodeName)\n        ) {\n            previousElementSiblingClosestBlock.remove();\n            setSelection(this, 0);\n            return;\n        }\n\n        /**\n         * Backspace at the beginning of a block node. If it doesn't have a left\n         * block and it is one of the special block formatting tags below then\n         * convert the block into a P and return immediately. Otherwise, we have\n         * to move the inline content at its beginning outside of the element\n         * and propagate to the left block.\n         *\n         * E.g. (prev == block)\n         *      <p>abc</p><div>[]def<p>ghi</p></div> + BACKSPACE\n         * <=>  <p>abc</p>[]def<div><p>ghi</p></div> + BACKSPACE\n         *\n         * E.g. (prev != block)\n         *      abc<div>[]def<p>ghi</p></div> + BACKSPACE\n         * <=>  abc[]def<div><p>ghi</p></div>\n         */\n        if (\n            !this.previousElementSibling &&\n            paragraphRelatedElements.includes(this.nodeName) &&\n            this.nodeName !== 'P' &&\n            !closestLi\n        ) {\n            if (!this.textContent) {\n                const p = document.createElement('p');\n                p.replaceChildren(...this.childNodes);\n                this.replaceWith(p);\n                setSelection(p, offset);\n            }\n            return;\n        } else {\n            moveDest = leftPos(this);\n        }\n    }\n\n    const domPathGenerator = createDOMPathGenerator(DIRECTIONS.LEFT, {\n        leafOnly: true,\n        stopTraverseFunction: isDeletable,\n    });\n    const domPath = domPathGenerator(this, offset)\n    const leftNode = domPath.next().value;\n    if (leftNode && isDeletable(leftNode)) {\n        const [parent, offset] = rightPos(leftNode);\n        return parent.oDeleteBackward(offset, alreadyMoved);\n    }\n    let node = this.childNodes[offset];\n    const nextSibling = this.nextSibling;\n    let currentNodeIndex = offset;\n\n    // `offsetLimit` will ensure we never move nodes that were not initialy in\n    // the element => when Deleting and merging an element the containing node\n    // will temporarily be hosted in the common parent beside possible other\n    // nodes. We don't want to touch those other nodes when merging two html\n    // elements ex : <div>12<p>ab[]</p><p>cd</p>34</div> should never touch the\n    // 12 and 34 text node.\n    if (offsetLimit === undefined) {\n        while (node && !isBlock(node)) {\n            node = node.nextSibling;\n            currentNodeIndex++;\n        }\n    } else {\n        currentNodeIndex = offsetLimit;\n    }\n    let [cursorNode, cursorOffset] = moveNodes(...moveDest, this, offset, currentNodeIndex);\n    setSelection(cursorNode, cursorOffset);\n\n    // Propagate if this is still a block on the left of where the nodes were\n    // moved.\n    if (\n        cursorNode.nodeType === Node.TEXT_NODE &&\n        (cursorOffset === 0 || cursorOffset === cursorNode.length)\n    ) {\n        cursorOffset = childNodeIndex(cursorNode) + (cursorOffset === 0 ? 0 : 1);\n        cursorNode = cursorNode.parentNode;\n    }\n    if (cursorNode.nodeType !== Node.TEXT_NODE) {\n        const { cType } = getState(cursorNode, cursorOffset, DIRECTIONS.LEFT);\n        if (cType & CTGROUPS.BLOCK && (!alreadyMoved || cType === CTYPES.BLOCK_OUTSIDE)) {\n            cursorNode.oDeleteBackward(cursorOffset, alreadyMoved, cursorOffset + currentNodeIndex - offset);\n        } else if (!alreadyMoved) {\n            // When removing a block node adjacent to an inline node, we need to\n            // ensure the block node induced line break are kept with a <br>.\n            // ex : <div>a<span>b</span><p>[]c</p>d</div> => deleteBakward =>\n            // <div>a<span>b</span>[]c<br>d</div> In this case we cannot simply\n            // merge the <p> content into the div parent, or we would lose the\n            // line break located after the <p>.\n            const cursorNodeNode = cursorNode.childNodes[cursorOffset];\n            const cursorNodeRightNode = cursorNodeNode ? cursorNodeNode.nextSibling : undefined;\n            if (cursorNodeRightNode &&\n                cursorNodeRightNode.nodeType === Node.TEXT_NODE &&\n                nextSibling === cursorNodeRightNode) {\n                moveDest[0].insertBefore(document.createElement('br'), cursorNodeRightNode);\n            }\n        }\n    }\n};\n\nHTMLLIElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {\n    // If the deleteBackward is performed at the begening of a LI element,\n    // we take the current LI out of the list.\n    if (offset === 0) {\n        this.oToggleList(offset);\n        return;\n    }\n    // Otherwise, call the HTMLElement deleteBackward method.\n    HTMLElement.prototype.oDeleteBackward.call(this, offset, alreadyMoved);\n};\n\nHTMLBRElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {\n    const parentOffset = childNodeIndex(this);\n    const rightState = getState(this.parentElement, parentOffset + 1, DIRECTIONS.RIGHT).cType;\n    if (rightState & CTYPES.BLOCK_INSIDE) {\n        this.parentElement.oDeleteBackward(parentOffset, alreadyMoved);\n    } else {\n        HTMLElement.prototype.oDeleteBackward.call(this, offset, alreadyMoved);\n    }\n};\n\nHTMLTableCellElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {\n    if (offset) {\n        HTMLElement.prototype.oDeleteBackward.call(this, offset, alreadyMoved);\n    }\n};\n", "/** @odoo-module **/\nimport { UNREMOVABLE_ROLLBACK_CODE } from '../utils/constants.js';\nimport {\n    findNode,\n    isSelfClosingElement,\n    nodeSize,\n    rightPos,\n    getState,\n    DIRECTIONS,\n    CTYPES,\n    leftPos,\n    isIconElement,\n    rightLeafOnlyNotBlockNotEditablePath,\n    rightLeafOnlyPathNotBlockNotEditablePath,\n    isNotEditableNode,\n    splitTextNode,\n    paragraphRelatedElements,\n    prepareUpdate,\n    isInPre,\n    fillEmpty,\n    setSelection,\n    isZWS,\n    childNodeIndex,\n    boundariesOut,\n    isEditorTab,\n    isVisible,\n    isUnbreakable,\n    isEmptyBlock,\n    isWhitespace,\n    isVisibleTextNode,\n    getOffsetAndCharSize,\n    ZERO_WIDTH_CHARS,\n} from '../utils/utils.js';\n\n/**\n * Handle text node deletion for Text.oDeleteForward and Text.oDeleteBackward.\n *\n * @param {int} charSize\n * @param {int} offset\n * @param {DIRECTIONS} direction\n * @param {boolean} alreadyMoved\n */\nexport function deleteText(charSize, offset, direction, alreadyMoved) {\n    const parentElement = this.parentElement;\n    // Split around the character where the deletion occurs.\n    const firstSplitOffset = splitTextNode(this, offset);\n    const secondSplitOffset = splitTextNode(parentElement.childNodes[firstSplitOffset], charSize);\n    const middleNode = parentElement.childNodes[firstSplitOffset];\n\n    // Do remove the character, then restore the state of the surrounding parts.\n    const restore = prepareUpdate(parentElement, firstSplitOffset, parentElement, secondSplitOffset);\n    const isSpace = isWhitespace(middleNode) && !isInPre(middleNode);\n    const isZWS = ZERO_WIDTH_CHARS.includes(middleNode.nodeValue);\n    middleNode.remove();\n    restore();\n\n    // If the removed element was not visible content, propagate the deletion.\n    const parentState = getState(parentElement, firstSplitOffset, direction);\n    if (\n        isZWS ||\n        (isSpace &&\n            (parentState.cType !== CTYPES.CONTENT || parentState.node === undefined))\n    ) {\n        if (direction === DIRECTIONS.LEFT) {\n            parentElement.oDeleteBackward(firstSplitOffset, alreadyMoved);\n        } else {\n            if (isSpace && parentState.node == undefined) {\n                // multiple invisible space at the start of the node\n                this.oDeleteForward(offset, alreadyMoved);\n            } else {\n                parentElement.oDeleteForward(firstSplitOffset, alreadyMoved);\n            }\n        }\n        if (isZWS && parentElement.isConnected) {\n            fillEmpty(parentElement);\n        }\n        return;\n    }\n    fillEmpty(parentElement);\n    setSelection(parentElement, firstSplitOffset);\n}\n\nText.prototype.oDeleteForward = function (offset, alreadyMoved = false) {\n    const parentElement = this.parentElement;\n\n    if (offset === this.nodeValue.length) {\n        // Delete at the end of a text node is not a specific case to handle,\n        // let the element implementation handle it.\n        parentElement.oDeleteForward([...parentElement.childNodes].indexOf(this) + 1);\n        return;\n    }\n    // Get the size of the unicode character to remove.\n    const [newOffset, charSize] = getOffsetAndCharSize(this.nodeValue, offset + 1, DIRECTIONS.RIGHT);\n    deleteText.call(this, charSize, newOffset, DIRECTIONS.RIGHT, alreadyMoved);\n};\n\nHTMLElement.prototype.oDeleteForward = function (offset) {\n    const filterFunc = node =>\n        isSelfClosingElement(node) || isVisibleTextNode(node) || isNotEditableNode(node);\n\n    const firstLeafNode = findNode(rightLeafOnlyNotBlockNotEditablePath(this, offset), filterFunc);\n    if (firstLeafNode &&\n        isZWS(firstLeafNode) &&\n        this.parentElement.hasAttribute('data-oe-zws-empty-inline')\n    ) {\n        const grandparent = this.parentElement.parentElement;\n        if (!grandparent) {\n            return;\n        }\n\n        const parentIndex = childNodeIndex(this.parentElement);\n        const restore = prepareUpdate(...boundariesOut(this.parentElement));\n        this.parentElement.remove();\n        restore();\n        HTMLElement.prototype.oDeleteForward.call(grandparent, parentIndex);\n        return;\n    } else if (\n        firstLeafNode &&\n        firstLeafNode.nodeType === Node.TEXT_NODE &&\n        firstLeafNode.textContent === '\\ufeff'\n    ) {\n        firstLeafNode.oDeleteForward(1);\n        return;\n    }\n    if (\n        this.hasAttribute &&\n        this.hasAttribute('data-oe-zws-empty-inline') &&\n        (\n            isZWS(this) ||\n            (this.textContent === '' && this.childNodes.length === 0)\n        )\n    ) {\n        const parent = this.parentElement;\n        if (!parent) {\n            return;\n        }\n\n        const index = childNodeIndex(this);\n        const restore = prepareUpdate(...boundariesOut(this));\n        this.remove();\n        restore();\n        HTMLElement.prototype.oDeleteForward.call(parent, index);\n        return;\n    }\n\n    if (firstLeafNode && (isIconElement(firstLeafNode) || isNotEditableNode(firstLeafNode))) {\n        const nextSibling = firstLeafNode.nextSibling;\n        const nextSiblingText = nextSibling ? nextSibling.textContent : '';\n        firstLeafNode.remove();\n        if (isEditorTab(firstLeafNode) && nextSiblingText[0] === '\\u200B') {\n            // When deleting an editor tab, we need to ensure it's related ZWS\n            // il deleted as well.\n            nextSibling.textContent = nextSiblingText.replace('\\u200B', '');\n        }\n        return;\n    }\n    if (\n        firstLeafNode &&\n        (firstLeafNode.nodeName !== 'BR' ||\n            getState(...rightPos(firstLeafNode), DIRECTIONS.RIGHT).cType !== CTYPES.BLOCK_INSIDE)\n    ) {\n        firstLeafNode.oDeleteBackward(Math.min(1, nodeSize(firstLeafNode)));\n        return;\n    }\n\n    let nextSibling = this.nextSibling;\n    while (nextSibling && isWhitespace(nextSibling)) {\n        const index = childNodeIndex(nextSibling);\n        const left = getState(nextSibling, index, DIRECTIONS.LEFT).cType;\n        const right = getState(nextSibling, index, DIRECTIONS.RIGHT).cType;\n        if (left === CTYPES.BLOCK_OUTSIDE && right === CTYPES.BLOCK_OUTSIDE) {\n            // If the next sibling is a whitespace, remove it.\n            nextSibling.remove();\n            nextSibling = this.nextSibling;\n        } else {\n            break;\n        }\n    }\n\n    if (\n        (\n            offset === this.childNodes.length ||\n            (this.childNodes.length === 1 && this.childNodes[0].tagName === 'BR')\n        ) &&\n        this.parentElement &&\n        nextSibling &&\n        ['LI', 'UL', 'OL'].includes(nextSibling.tagName)\n    ) {\n        const nextSiblingNestedLi = nextSibling.querySelector('li:first-child');\n        if (nextSiblingNestedLi) {\n            // Add the first LI from the next sibbling list to the current list.\n            this.after(nextSiblingNestedLi);\n            // Remove the next sibbling list if it's empty.\n            if (!isVisible(nextSibling, false) || nextSibling.textContent === '') {\n                nextSibling.remove();\n            }\n            HTMLElement.prototype.oDeleteBackward.call(nextSiblingNestedLi, 0, true);\n        } else {\n            HTMLElement.prototype.oDeleteBackward.call(nextSibling, 0);\n        }\n        return;\n    }\n\n    // Remove the nextSibling if it is a non-editable element.\n    if (\n        nextSibling &&\n        nextSibling.nodeType === Node.ELEMENT_NODE &&\n        !nextSibling.isContentEditable\n    ) {\n        nextSibling.remove();\n        return;\n    }\n    const parentEl = this.parentElement;\n    // Prevent the deleteForward operation since it is done at the end of an\n    // enclosed editable zone (inside a non-editable zone in the editor).\n    if (\n        parentEl &&\n        parentEl.getAttribute(\"contenteditable\") === \"true\" &&\n        parentEl.oid !== \"root\" &&\n        parentEl.parentElement &&\n        !parentEl.parentElement.isContentEditable &&\n        paragraphRelatedElements.includes(this.tagName) &&\n        !this.nextElementSibling\n    ) {\n        throw UNREMOVABLE_ROLLBACK_CODE;\n    }\n    const firstOutNode = findNode(\n        rightLeafOnlyPathNotBlockNotEditablePath(\n            ...(firstLeafNode ? rightPos(firstLeafNode) : [this, offset]),\n        ),\n        filterFunc,\n    );\n    if (firstOutNode) {\n        // If next sibblings is an unbreadable node, and current node is empty, we\n        // delete the current node and put the selection at the beginning of the\n        // next sibbling.\n        if (nextSibling && isUnbreakable(nextSibling) && isEmptyBlock(this)) {\n            const restore = prepareUpdate(...boundariesOut(this));\n            this.remove();\n            restore();\n            setSelection(firstOutNode, 0);\n            return;\n        }\n        const [node, offset] = leftPos(firstOutNode);\n        // If the next node is a <LI> we call directly the htmlElement\n        // oDeleteBackward : because we don't want the special cases of\n        // deleteBackward for LI when we comme from a deleteForward.\n        if (node.tagName === 'LI') {\n            HTMLElement.prototype.oDeleteBackward.call(node, offset);\n            return;\n        }\n        node.oDeleteBackward(offset);\n        return;\n    }\n};\n", "/** @odoo-module **/\nimport { UNBREAKABLE_ROLLBACK_CODE } from '../utils/constants.js';\n\nimport {\n    childNodeIndex,\n    fillEmpty,\n    isBlock,\n    isUnbreakable,\n    prepareUpdate,\n    setCursorStart,\n    setCursorEnd,\n    setTagName,\n    splitTextNode,\n    toggleClass,\n    isVisible,\n    descendants,\n    isVisibleTextNode,\n    nodeSize,\n    getTraversedNodes,\n    setSelection,\n} from '../utils/utils.js';\n\nText.prototype.oEnter = function (offset) {\n    this.parentElement.oEnter(splitTextNode(this, offset), true);\n};\n/**\n * The whole logic can pretty much be described by this example:\n *\n *     <p><span><b>[]xt</b>ab</span>cd</p> + ENTER\n * <=> <p><span><b><br></b>[]<b>xt</b>ab</span>cd</p> + ENTER\n * <=> <p><span><b><br></b></span>[]<span><b>xt</b>ab</span>cd</p> + ENTER\n * <=> <p><span><b><br></b></span></p><p><span><b>[]xt</b>ab</span>cd</p> + SANITIZE\n * <=> <p><br></p><p><span><b>[]xt</b>ab</span>cd</p>\n *\n * Propagate the split for as long as we split an inline node, then refocus the\n * beginning of the first split node\n */\nHTMLElement.prototype.oEnter = function (offset, firstSplit = true) {\n    let didSplit = false;\n    if (isUnbreakable(this)) {\n        throw UNBREAKABLE_ROLLBACK_CODE;\n    }\n    if (\n        !this.textContent &&\n        ['BLOCKQUOTE', 'PRE'].includes(this.parentElement.nodeName) &&\n        !this.nextSibling\n    ) {\n        const parent = this.parentElement;\n        const index = childNodeIndex(this);\n        if (this.previousElementSibling) {\n            this.remove();\n            return parent.oEnter(index, !didSplit);\n        }\n        return parent.oEnter(index + 1, !didSplit);\n    }\n    let restore;\n    if (firstSplit) {\n        restore = prepareUpdate(this, offset);\n    }\n\n    // First split the node in two and move half the children in the clone.\n    let splitEl = this.cloneNode(false);\n    while (offset < this.childNodes.length) {\n        splitEl.appendChild(this.childNodes[offset]);\n    }\n    if (isBlock(this) || splitEl.hasChildNodes()) {\n        this.after(splitEl);\n        if (isBlock(splitEl) || isVisible(splitEl) || splitEl.textContent === '\\u200B') {\n            didSplit = true;\n        } else {\n            splitEl.remove();\n        }\n    }\n\n    // Propagate the split until reaching a block element (or continue to the\n    // closest list item element if there is one).\n    if (!isBlock(this) || (this.nodeName !== 'LI' && this.closest('LI'))) {\n        if (this.parentElement) {\n            this.parentElement.oEnter(childNodeIndex(this) + 1, !didSplit);\n        } else {\n            // There was no block parent element in the original chain, consider\n            // this unsplittable, like an unbreakable.\n            throw UNBREAKABLE_ROLLBACK_CODE;\n        }\n    }\n\n    // All split have been done, place the cursor at the right position, and\n    // fill/remove empty nodes.\n    if (firstSplit && didSplit) {\n        restore();\n\n        let node = this;\n        while (!isBlock(node) && !isVisible(node)) {\n            const toRemove = node;\n            node = node.parentNode;\n            toRemove.remove();\n        }\n        fillEmpty(node);\n        fillEmpty(splitEl);\n        if (splitEl.tagName === 'A') {\n            while (!isBlock(splitEl) && !isVisible(splitEl)) {\n                const toRemove = splitEl;\n                splitEl = splitEl.parentNode;\n                toRemove.remove();\n            }\n        }\n        setCursorStart(splitEl);\n    }\n    return splitEl;\n};\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 */\nHTMLHeadingElement.prototype.oEnter = function () {\n    const newEl = HTMLElement.prototype.oEnter.call(this, ...arguments);\n    if (newEl && !descendants(newEl).some(isVisibleTextNode)) {\n        const node = setTagName(newEl, 'P');\n        node.replaceChildren(document.createElement('br'));\n        setCursorStart(node);\n    }\n};\nconst isAtEdgeofLink = (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}\nHTMLAnchorElement.prototype.oEnter = function (offset) {\n    const edge = isAtEdgeofLink(this, offset);\n    if (edge === 'start') {\n        // Do not break the link at the edge: break before it.\n        if (this.previousSibling) {\n            return HTMLElement.prototype.oEnter.call(this.previousSibling, nodeSize(this.previousSibling));\n        } else {\n            const index = childNodeIndex(this);\n            return HTMLElement.prototype.oEnter.call(this.parentElement, index ? index - 1 : 0);\n        }\n    } else if (edge === 'end') {\n        // Do not break the link at the edge: break after it.\n        if (this.nextSibling) {\n            return HTMLElement.prototype.oEnter.call(this.nextSibling, 0);\n        } else {\n            return HTMLElement.prototype.oEnter.call(this.parentElement, childNodeIndex(this));\n        }\n    } else {\n        HTMLElement.prototype.oEnter.call(this, ...arguments);\n    }\n}\n/**\n * Same specific behavior as headings elements.\n */\nHTMLQuoteElement.prototype.oEnter = HTMLHeadingElement.prototype.oEnter;\n/**\n * Specific behavior for list items: deletion and unindentation when empty.\n */\nHTMLLIElement.prototype.oEnter = function () {\n    // If not empty list item, regular block split\n    const traverseNodes = getTraversedNodes(this);\n    const isContainUnbreakable = traverseNodes.some(isUnbreakable);\n    if (this.textContent || isContainUnbreakable) {\n        const node = HTMLElement.prototype.oEnter.call(this, ...arguments);\n        if (node.classList.contains('o_checked')) {\n            toggleClass(node, 'o_checked');\n        }\n        return node;\n    }\n    this.oShiftTab();\n};\n/**\n * Specific behavior for pre: insert newline (\\n) in text or insert p at end.\n */\nHTMLPreElement.prototype.oEnter = function (offset) {\n    if (offset < this.childNodes.length) {\n        const lineBreak = document.createElement('br');\n        this.insertBefore(lineBreak, this.childNodes[offset]);\n        setCursorEnd(lineBreak);\n    } else {\n        if (this.parentElement.nodeName === 'LI') {\n            setSelection(this.parentElement, childNodeIndex(this) + 1);\n            HTMLLIElement.prototype.oEnter.call(this.parentElement, ...arguments);\n            return;\n        }\n        const node = document.createElement('p');\n        this.parentNode.insertBefore(node, this.nextSibling);\n        fillEmpty(node);\n        setCursorStart(node);\n    }\n};\n", "/** @odoo-module **/\nimport {\n    CTYPES,\n    DIRECTIONS,\n    isFakeLineBreak,\n    prepareUpdate,\n    rightPos,\n    setSelection,\n    getState,\n    leftPos,\n    splitTextNode,\n} from '../utils/utils.js';\n\nText.prototype.oShiftEnter = function (offset) {\n    return this.parentElement.oShiftEnter(splitTextNode(this, offset));\n};\n\nHTMLElement.prototype.oShiftEnter = function (offset) {\n    const restore = prepareUpdate(this, offset);\n\n    const brEl = document.createElement('br');\n    const brEls = [brEl];\n    if (offset >= this.childNodes.length) {\n        this.appendChild(brEl);\n    } else {\n        this.insertBefore(brEl, this.childNodes[offset]);\n    }\n    if (isFakeLineBreak(brEl) && getState(...leftPos(brEl), DIRECTIONS.LEFT).cType !== CTYPES.BR) {\n        const brEl2 = document.createElement('br');\n        brEl.before(brEl2);\n        brEls.unshift(brEl2);\n    }\n\n    restore();\n\n    for (const el of brEls) {\n        if (el.parentNode) {\n            setSelection(...rightPos(el));\n            break;\n        }\n    }\n\n    return brEls;\n};\n\n/**\n * Special behavior for links: do not add a line break at its edges, but rather\n * move the line break outside the link.\n */\nHTMLAnchorElement.prototype.oShiftEnter = function () {\n    const brs = HTMLElement.prototype.oShiftEnter.call(this, ...arguments);\n    const anchor = brs[0].parentElement;\n    let firstChild = anchor.firstChild;\n    if (firstChild && firstChild.nodeType === Node.TEXT_NODE && firstChild.textContent === '\\uFEFF') {\n        firstChild = anchor.childNodes[1];\n    }\n    let lastChild = anchor.lastChild;\n    if (lastChild && lastChild.nodeType === Node.TEXT_NODE && lastChild.textContent === '\\uFEFF') {\n        lastChild = anchor.childNodes.length > 1 && anchor.childNodes[anchor.childNodes.length - 2];\n    }\n    if (brs.includes(firstChild)) {\n        brs.forEach(br => anchor.before(br));\n        setSelection(...rightPos(brs[brs.length - 1]));\n    } else if (brs.includes(lastChild)) {\n        brs.forEach(br => anchor.after(br));\n        setSelection(...rightPos(brs[0]));\n    }\n}\n", "/** @odoo-module **/\nimport { isUnbreakable, preserveCursor, toggleClass, isBlock, isVisible } from '../utils/utils.js';\n\nText.prototype.oShiftTab = function () {\n    return this.parentElement.oShiftTab(0);\n};\n\nHTMLElement.prototype.oShiftTab = function (offset = undefined) {\n    if (!isUnbreakable(this)) {\n        return this.parentElement.oShiftTab(offset);\n    }\n    return false;\n};\n\n// returns: is still in a <LI> nested list\nHTMLLIElement.prototype.oShiftTab = function () {\n    const li = this;\n    if (li.nextElementSibling) {\n        const ul = li.parentElement.cloneNode(false);\n        while (li.nextSibling) {\n            ul.append(li.nextSibling);\n        }\n        if (li.parentNode.parentNode.tagName === 'LI') {\n            const lip = document.createElement('li');\n            toggleClass(lip, 'oe-nested');\n            lip.append(ul);\n            li.parentNode.parentNode.after(lip);\n        } else {\n            li.parentNode.after(ul);\n        }\n    }\n\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    if (\n        li.parentNode.parentNode.tagName === 'LI' &&\n        !li.parentNode.parentNode.classList.contains('nav-item')\n    ) {\n        const ul = li.parentNode;\n        const shouldRemoveParentLi = !li.previousElementSibling && !ul.previousElementSibling;\n        const toremove = shouldRemoveParentLi ? ul.parentNode : null;\n        ul.parentNode.after(li);\n        if (toremove) {\n            if (toremove.classList.contains('oe-nested')) {\n                // <li>content<ul>...</ul></li>\n                toremove.remove();\n            } else {\n                // <li class=\"oe-nested\"><ul>...</ul></li>\n                ul.remove();\n            }\n        }\n        restoreCursor();\n        return li;\n    } else {\n        const ul = li.parentNode;\n        const dir = ul.getAttribute('dir');\n        let p;\n        while (li.firstChild) {\n            if (isBlock(li.firstChild)) {\n                if (p && isVisible(p)) {\n                    ul.after(p);\n                }\n                p = undefined;\n                ul.after(li.firstChild);\n            } else {\n                p = p || document.createElement('P');\n                if (dir) {\n                    p.setAttribute('dir', dir);\n                    p.style.setProperty('text-align', ul.style.getPropertyValue('text-align'));\n                }\n                p.append(li.firstChild);\n            }\n        }\n        if (p && isVisible(p)) {\n            ul.after(p)\n        }\n\n        restoreCursor(new Map([[li, ul.nextSibling]]));\n        li.remove();\n        if (!ul.firstElementChild) {\n            ul.remove();\n        }\n    }\n    return false;\n};\n", "/** @odoo-module **/\nimport { createList, getListMode, isBlock, preserveCursor, toggleClass } from '../utils/utils.js';\n\nText.prototype.oTab = function () {\n    return this.parentElement.oTab(0);\n};\n\nHTMLElement.prototype.oTab = function (offset) {\n    if (!isBlock(this)) {\n        return this.parentElement.oTab(offset);\n    }\n    return false;\n};\n\nHTMLLIElement.prototype.oTab = function () {\n    const lip = document.createElement('li');\n    const destul =\n        (this.previousElementSibling && this.previousElementSibling.querySelector('ol, ul')) ||\n        (this.nextElementSibling && this.nextElementSibling.querySelector('ol, ul')) ||\n        this.closest('ul, ol');\n\n    const ul = createList(getListMode(destul));\n    lip.append(ul);\n\n    const cr = preserveCursor(this.ownerDocument);\n    toggleClass(lip, 'oe-nested');\n    this.before(lip);\n    ul.append(this);\n    cr();\n    return true;\n};\n", "/** @odoo-module **/\nimport {\n    childNodeIndex,\n    isBlock,\n    preserveCursor,\n    insertListAfter,\n    getAdjacents,\n    closestElement,\n    toggleList,\n} from '../utils/utils.js';\n\nText.prototype.oToggleList = function (offset, mode) {\n    // Create a new list if textNode is inside a nav-item list\n    if (closestElement(this, 'li').classList.contains('nav-item')) {\n        const restoreCursor = preserveCursor(this.ownerDocument);\n        insertListAfter(this, mode, [this]);\n        restoreCursor();\n    } else {\n        this.parentElement.oToggleList(childNodeIndex(this), mode);\n    }\n};\n\nHTMLElement.prototype.oToggleList = function (offset, mode = 'UL') {\n    if (!isBlock(this)) {\n        return this.parentElement.oToggleList(childNodeIndex(this));\n    }\n    const closestLi = this.closest('li');\n    // Do not toggle nav-item list as they don't behave like regular list items\n    if (closestLi && !closestLi.classList.contains('nav-item')) {\n        return closestLi.oToggleList(0, mode);\n    }\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    if (this.oid === 'root') {\n        const callingNode = this.childNodes[offset];\n        const group = getAdjacents(callingNode, n => !isBlock(n));\n        insertListAfter(callingNode, mode, [group]);\n        restoreCursor();\n    } else {\n        const list = insertListAfter(this, mode, [this]);\n        if (this.hasAttribute('dir')) {\n            list.setAttribute('dir', this.getAttribute('dir'));\n        }\n        restoreCursor(new Map([[this, list.firstElementChild]]));\n    }\n};\n\nHTMLParagraphElement.prototype.oToggleList = function (offset, mode = 'UL') {\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    const list = insertListAfter(this, mode, [[...this.childNodes]]);\n    const classList = [...list.classList];\n    for (const attribute of this.attributes) {\n        if (attribute.name === 'class' && attribute.value && list.className) {\n            list.className = `${list.className} ${attribute.value}`;\n        } else {\n            list.setAttribute(attribute.name, attribute.value);\n        }\n    }\n    for (const className of classList) {\n        list.classList.toggle(className, true); // restore list classes\n    }\n    this.remove();\n\n    restoreCursor(new Map([[this, list.firstChild]]));\n    return true;\n};\n\nHTMLLIElement.prototype.oToggleList = function (offset, mode) {\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    toggleList(this, mode, offset);\n    restoreCursor();\n    return false;\n};\n\nHTMLTableCellElement.prototype.oToggleList = function (offset, mode) {\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    const callingNode = this.childNodes[offset];\n    const group = getAdjacents(callingNode, n => !isBlock(n));\n    insertListAfter(callingNode, mode, [group]);\n    restoreCursor();\n};\n", "/** @odoo-module **/\nimport { 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, speed: 20 },\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, addClass }) => {\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            addClass(frameElement, \"pe-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 (ctx.current.dropzone?.el && ctx.current.dropzone.el.classList.contains(\"oe_grid_zone\")) {\n            ctx.current.dropzone.rect = ctx.current.dropzone.el.getBoundingClientRect();\n        }\n        if (\n            ctx.current.dropzone &&\n            (\n                ctx.current.dropzone.el === dropzoneEl\n                || (\n                    !dropzoneEl\n                    && touching([ctx.current.helper], ctx.current.dropzone.rect).length > 0\n                )\n            )\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, y: rect.y, width: rect.width, 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", "/** @odoo-module **/\n\nimport { registry } from '@web/core/registry'\nimport { HotkeyCommandItem } from '@web/core/commands/default_providers'\nimport { Wysiwyg } from '@web_editor/js/wysiwyg/wysiwyg';\n\n// The only way to know if an editor is under focus when the command palette\n// open is to look if there in a selection within a wysiwyg editor in the page.\n// As the selection changes after the command palette is open, we need to save\n// the action (that have the range and editor in the closure) as well as the\n// label to use.\nlet sessionActionLabel = [];\n\nconst commandProviderRegistry = registry.category(\"command_provider\");\ncommandProviderRegistry.add(\"link dialog\", {\n    async provide(env, { sessionId }) {\n        let [lastSessionId, action, label] = sessionActionLabel;\n        if (lastSessionId !== sessionId) {\n            const wysiwyg = [...Wysiwyg.activeWysiwygs].find((wysiwyg) => {\n                return wysiwyg.isSelectionInEditable();\n            });\n            const selection = wysiwyg && wysiwyg.odooEditor && wysiwyg.odooEditor.document.getSelection();\n            const range = selection && selection.rangeCount && selection.getRangeAt(0);\n            if (range) {\n                label = !wysiwyg.getInSelection('a') ? 'Create link' : 'Edit link';\n                action = () => {\n                    const selection = wysiwyg.odooEditor.document.getSelection();\n                    selection.removeAllRanges();\n                    selection.addRange(range);\n\n                    wysiwyg.openLinkToolsFromSelection();\n                }\n                sessionActionLabel = [sessionId, action, label]\n            } else {\n                sessionActionLabel = [sessionId];\n            }\n        }\n        [lastSessionId, action, label] = sessionActionLabel;\n\n        if (action) {\n            return [\n                {\n                    Component: HotkeyCommandItem,\n                    action: action,\n                    category: 'shortcut_conflict',\n                    name: label,\n                    props: { hotkey: 'control+k' },\n                }\n            ]\n        } else {\n            return [];\n        }\n    },\n});\n", "/** @odoo-module */\nimport {\n    ancestors,\n    closestElement,\n    resetOuids,\n    setSelection,\n} from '@web_editor/js/editor/odoo-editor/src/OdooEditor';\nimport { useNativeDraggable } from \"@web_editor/js/editor/drag_and_drop\";\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\nconst WIDGET_CONTAINER_WIDTH = 25;\nconst WIDGET_MOVE_SIZE = 20;\n\nconst ALLOWED_ELEMENTS = 'h1, h2, h3, p, hr, pre, blockquote, ul, ol, table, .o_knowledge_behavior_anchor, .o_text_columns, .o_editor_banner, .oe_movable';\n\nexport class MoveNodePlugin {\n    constructor(options = {}) {\n        this._options = options;\n\n        this._intersectionObserver = new IntersectionObserver(\n            this._intersectionObserverCallback.bind(this),\n            {\n                root: document,\n            }\n        );\n        this._visibleMovableElements = new Set();\n    }\n\n    start() {\n        this._editor = this._options.editor;\n        this._editable = this._options.editor.editable;\n        this._document = this._options.editor.document;\n        this._elementHookMap = new Map();\n\n        this._editor.addDomListener(this._editable, 'mousemove', this._onMousemove.bind(this), true);\n        this._editor.addDomListener(this._editor.document, 'keydown', this._onDocumentKeydown.bind(this), true);\n        this._editor.addDomListener(this._editor.document, 'mousemove', this._onDocumentMousemove.bind(this), true);\n\n        const avatarContainer = this._editor.mainAbsoluteContainer.querySelector('[data-oe-absolute-container-id=\"oe-avatars-counters-container\"]');\n\n        // This container help to add zone into which the mouse can activate the move widget.\n        this._widgetHookContainer = this._editor.makeAbsoluteContainer('oe-widget-hooks-container');\n        avatarContainer.before(this._widgetHookContainer);\n        // This container contains the differents widgets.\n        this._widgetContainer = this._editor.makeAbsoluteContainer('oe-widgets-container');\n        avatarContainer.before(this._widgetContainer);\n        // This container contains the jquery helper element.\n        this._dragHelperContainer = this._editor.makeAbsoluteContainer('oe-movenode-helper-container');\n        avatarContainer.before(this._dragHelperContainer);\n        // This container contains drop zones. They are the zones that handle where the drop should happen.\n        this._dropzonesContainer = this._editor.makeAbsoluteContainer('oe-dropzones-container');\n        avatarContainer.before(this._dropzonesContainer);\n        // This container contains drop hint. The final rectangle showed to the user.\n        this._dropzoneHintContainer = this._editor.makeAbsoluteContainer('oe-dropzone-hint-container');\n        avatarContainer.before(this._dropzoneHintContainer);\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 (this._scrollableElement && getComputedStyle(this._scrollableElement).overflowY !== 'auto') {\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        this._editor.addDomListener(window, 'resize', this._updateHooks.bind(this));\n        if (this._editor.document.defaultView !== window) {\n            this._editor.addDomListener(this._editor.document.defaultView, 'resize', this._updateHooks.bind(this));\n        }\n    }\n    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 = 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 = newAnchorWidget && closestElement(newAnchorWidget, (node) => {\n            return isNodeMovable(node) && node.matches(ALLOWED_ELEMENTS);\n        });\n        // Retrive the first list container from the ancestors.\n        const listContainer = movableElement && 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        return [...new Set([...this._editable.querySelectorAll(ALLOWED_ELEMENTS)])]\n            .filter((node) => isNodeMovable(node));\n    }\n    _getDroppableElements(draggableNode) {\n        return this._getMovableElements().filter((node) =>\n            !closestElement(node.parentElement, (n) => n === draggableNode)\n        );\n    }\n    _setMovableElement(movableElement) {\n        this._removeMoveWidget();\n        this._currentMovableElement = movableElement;\n        this._editor.disableAvatarForElement(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._editor.enableAvatars();\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                sideElement.addEventListener('mouseenter', () => {\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                const removeDropHint = () => {\n                    if (this._currentDropHint) {\n                        this._currentDropHint.remove();\n                        this._currentDropHint = null;\n                    }\n                    this._currentDropHintCommand = null;\n                }\n                dropzoneBox.addEventListener('mouseleave', 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 p = document.createElement('p');\n                const br = document.createElement('br');\n                p.append(br);\n                previousParent.append(p);\n            }\n            setSelection(\n                movableElement,\n                movableElement.childNodes.length\n            );\n            resetOuids(movableElement);\n            this._editor.historyStep();\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        if (this._editableRect && !isPointInside(this._editableRect, e.clientX, e.clientY)) {\n            this._removeMoveWidget();\n        }\n    }\n}\n\nfunction isNodeMovable(node) {\n    return node.parentElement?.getAttribute('contentEditable') === 'true' && !node.parentElement.closest('.o_editor_banner');\n}\n\nfunction isPointInside(rect, x, y) {\n    return rect.left <= x &&\n        rect.right >= x &&\n        rect.top <= y &&\n        rect.bottom >= y;\n};\n", "/** @odoo-module */\nimport { browser } from \"@web/core/browser/browser\";\nconst localStorage = browser.localStorage;\n\nconst 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.fromClientId,\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 clientId = notification.fromClientId;\n        if (this.clientsInfos[clientId] && this.clientsInfos[clientId].peerConnection) {\n            return this.clientsInfos[clientId];\n        }\n        this._createClient(clientId);\n    },\n\n    rtc_signal_icecandidate: async function (notification) {\n        if (debugShowLog) console.log(`%creceive candidate`, 'background: darkgreen; color: white;');\n        const clientInfos = this.clientsInfos[notification.fromClientId];\n        if (\n            !clientInfos ||\n            !clientInfos.peerConnection ||\n            clientInfos.peerConnection.connectionState === 'closed'\n        ) {\n            console.groupCollapsed('=== ERROR: Handle Ice Candidate from undefined|closed ===');\n            console.trace(clientInfos);\n            console.groupEnd();\n            return;\n        }\n        if (!clientInfos.peerConnection.remoteDescription) {\n            clientInfos.iceCandidateBuffer.push(notification.notificationPayload);\n        } else {\n            this._addIceCandidate(clientInfos, 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        const clientInfos =\n            this.clientsInfos[notification.fromClientId] ||\n            this._createClient(notification.fromClientId);\n        const pc = clientInfos.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 - client: ' +\n                        notification.fromClientId,\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 client that abort by\n        // rollbacking to a stable signaling state where the other is\n        // continuing the process. The client that is polite is the one that\n        // will rollback.\n        const isPolite =\n            ('' + notification.fromClientId).localeCompare('' + this._currentClientId) === 1;\n        if (debugShowLog)\n            console.log(\n                `%cisPolite: %c${isPolite}`,\n                'background: deepskyblue;',\n                `background:${isPolite ? 'green' : 'red'}`,\n            );\n\n        const isOfferRacing =\n            description.type === 'offer' &&\n            (clientInfos.makingOffer || pc.signalingState !== 'stable');\n        // If there is a racing conditing with the signaling offer and the\n        // client 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            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 (clientInfos.iceCandidateBuffer.length) {\n            for (const candidate of clientInfos.iceCandidateBuffer) {\n                await this._addIceCandidate(clientInfos, candidate);\n            }\n            clientInfos.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.notifyClient(\n                notification.fromClientId,\n                'rtc_signal_description',\n                pc.localDescription,\n            );\n        }\n    },\n};\n\nexport class PeerToPeer {\n    constructor(options) {\n        this.options = options;\n        this._currentClientId = this.options.currentClientId;\n        if (debugShowLog)\n            console.log(\n                `%c currentClientId:${this._currentClientId}`,\n                'background: blue; color: white;',\n            );\n\n        // clientId -> ClientInfos\n        this.clientsInfos = {};\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    getConnectedClientIds() {\n        return Object.entries(this.clientsInfos)\n            .filter(\n                ([id, infos]) =>\n                    infos.peerConnection && infos.peerConnection.iceConnectionState === 'connected' &&\n                    infos.dataChannel && infos.dataChannel.readyState === 'open',\n            )\n            .map(([id]) => id);\n    }\n\n    removeClient(clientId) {\n        if (debugShowLog) console.log(`%c REMOVE CLIENT ${clientId}`, 'background: chocolate;');\n        this.notifySelf('ptp_remove', clientId);\n        const clientInfos = this.clientsInfos[clientId];\n        if (!clientInfos) return;\n        clearTimeout(clientInfos.fallbackTimeout);\n        clearTimeout(clientInfos.zombieTimeout);\n        clientInfos.dataChannel && clientInfos.dataChannel.close();\n        clientInfos.peerConnection && clientInfos.peerConnection.close();\n        delete this.clientsInfos[clientId];\n    }\n\n    closeAllConnections() {\n        for (const clientId of Object.keys(this.clientsInfos)) {\n            this.notifyAllClients('ptp_disconnect');\n            this.removeClient(clientId);\n        }\n    }\n\n    async notifyAllClients(notificationName, notificationPayload, { transport = 'server' } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        const transportPayload = {\n            fromClientId: this._currentClientId,\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.clientsInfos)) {\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    notifyClient(clientId, 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._currentClientId.slice('-5')}:${clientId.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._currentClientId.slice(\n                        '-5',\n                    )}:${clientId.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._currentClientId.slice(\n                        '-5',\n                    )}:${clientId.slice('-5')}`,\n                    'color: #aaa;font-weight:bold;',\n                    'color: #aaa;font-weight:normal',\n                );\n            }\n        }\n        const transportPayload = {\n            fromClientId: this._currentClientId,\n            toClientId: clientId,\n            notificationName,\n            notificationPayload,\n        };\n        if (transport === 'server') {\n            this.options.broadcastAll(transportPayload);\n        } else if (transport === 'rtc') {\n            this._channelNotify(clientId, 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.fromClientId === 'undefined' &&\n            typeof notification.toClientId === 'undefined';\n        if (\n            isInternalNotification ||\n            (notification.fromClientId !== this._currentClientId && !notification.toClientId) ||\n            notification.toClientId === this._currentClientId\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.fromClientId.slice('-5')}:${notification.toClientId.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.fromClientId.slice('-5')}:${notification.toClientId.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.fromClientId}:${notification.toClientId}`,\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    requestClient(clientId, 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.notifyClient(\n                clientId,\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    _createClient(clientId, { makeOffer = true } = {}) {\n        if (this._stopped) {\n            return;\n        }\n        if (debugShowLog) console.log('CREATE CONNECTION with client id:', clientId);\n        this.clientsInfos[clientId] = {\n            makingOffer: false,\n            iceCandidateBuffer: [],\n            backoffFactor: 0,\n        };\n\n        if (!navigator.onLine) {\n            return this.clientsInfos[clientId];\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                try {\n                    this.clientsInfos[clientId].makingOffer = true;\n                    if (debugShowLog)\n                        console.log(\n                            `%ccreating and sending an offer`,\n                            'background: darkmagenta; color: white;',\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.notifyClient(clientId, 'rtc_signal_description', pc.localDescription);\n                } catch (err) {\n                    console.error(err);\n                } finally {\n                    this.clientsInfos[clientId].makingOffer = false;\n                }\n            };\n        }\n        pc.onicecandidate = async event => {\n            if (event.candidate) {\n                this.notifyClient(clientId, 'rtc_signal_icecandidate', event.candidate);\n            }\n        };\n        pc.oniceconnectionstatechange = async () => {\n            if (debugShowLog) console.log('ICE STATE UPDATE: ' + pc.iceConnectionState);\n\n            switch (pc.iceConnectionState) {\n                case 'failed':\n                case 'closed':\n                    this.removeClient(clientId);\n                    break;\n                case 'disconnected':\n                    if (navigator.onLine) {\n                        await this._recoverConnection(clientId, {\n                            delay: 3000,\n                            reason: 'ice connection disconnected',\n                        });\n                    }\n                    break;\n                case 'connected':\n                    this.clientsInfos[clientId].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) console.log('CONNECTION STATE UPDATE:' + pc.connectionState);\n\n            switch (pc.connectionState) {\n                case 'failed':\n                case 'closed':\n                    this.removeClient(clientId);\n                    break;\n                case 'disconnected':\n                    if (navigator.onLine) {\n                        await this._recoverConnection(clientId, {\n                            delay: 3000,\n                            reason: 'connection disconnected',\n                        });\n                    }\n                    break;\n                case 'connected':\n                case 'completed':\n                    this.clientsInfos[clientId].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(clientId, { 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                connectionClientId: clientId,\n            });\n        };\n\n        this.clientsInfos[clientId].peerConnection = pc;\n        this.clientsInfos[clientId].dataChannel = dataChannel;\n\n        return this.clientsInfos[clientId];\n    }\n    async _addIceCandidate(clientInfos, candidate) {\n        const rtcIceCandidate = new RTCIceCandidate(candidate);\n        try {\n            await clientInfos.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(clientId, transportPayload) {\n        if (this._stopped) {\n            return;\n        }\n        const clientInfo = this.clientsInfos[clientId];\n        const dataChannel = clientInfo && clientInfo.dataChannel;\n\n        if (!dataChannel || dataChannel.readyState !== 'open') {\n            if (clientInfo && !clientInfo.zombieTimeout) {\n                if (debugShowLog) console.warn(\n                    `Impossible to communicate with client ${clientId}. The connection will be killed in 10 seconds if the datachannel state has not changed.`,\n                );\n                this._killPotentialZombie(clientId);\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(fromClientId, 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            fromClientId,\n            requestId,\n            requestName,\n            requestPayload,\n        });\n        this.notifyClient(\n            fromClientId,\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(clientId, { delay = 0, reason = '' } = {}) {\n        if (this._stopped) {\n            this.removeClient(clientId);\n            return;\n        }\n        const clientInfos = this.clientsInfos[clientId];\n        if (!clientInfos || clientInfos.fallbackTimeout) return;\n        const backoffFactor = this.clientsInfos[clientId].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 client ${clientId}: ${reason}`,\n                    'background: darkred; color: white;',\n                );\n            }\n            return;\n        }\n\n        clientInfos.fallbackTimeout = setTimeout(async () => {\n            clientInfos.fallbackTimeout = undefined;\n            const pc = clientInfos.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 client ${clientId} to salvage the connection ${pc.iceConnectionState} after ${backoffDelay}ms, reason: ${reason}`,\n                    'background: darkorange; color: white;',\n                );\n            this.removeClient(clientId);\n            const newClientInfos = this._createClient(clientId);\n            newClientInfos.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(clientId) {\n        if (this._stopped) {\n            this.removeClient(clientId);\n            return;\n        }\n        const clientInfos = this.clientsInfos[clientId];\n        if (!clientInfos || clientInfos.zombieTimeout) {\n            return;\n        }\n\n        // If there is no connection after 10 seconds, terminate.\n        clientInfos.zombieTimeout = setTimeout(() => {\n            if (clientInfos && clientInfos.dataChannel && clientInfos.dataChannel.readyState !== 'open') {\n                if (debugShowLog) console.log(`%c KILL ZOMBIE ${clientId}`, 'background: red;');\n                this.removeClient(clientId);\n            } else {\n                if (debugShowLog) console.log(`%c NOT A ZOMBIE ${clientId}`, 'background: green;');\n            }\n        }, 10000);\n    }\n}\n\nexport class RequestError extends Error {\n  constructor(message) {\n    super(message);\n    this.name = \"RequestError\";\n  }\n}\n", "/* @odoo-module */\n\nimport { Component } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class ConflictDialog extends Component {\n    static components = { Dialog };\n    static props = [\"close\",\"content\"];\n    static template = 'web_editor.ConflictDialog';\n}\n", "\n/** @odoo-module **/\nimport { registry } from \"@web/core/registry\";\n\nlet colorPickerTemplatePromise;\nexport const getColorPickerTemplateService = {\n    dependencies: [\"orm\"],\n    async: true,\n    start(env, { orm }) {\n        return () => {\n            colorPickerTemplatePromise ??= orm.call(\n                'ir.ui.view',\n                'render_public_asset',\n                ['web_editor.colorpicker', {}]\n            );\n            return colorPickerTemplatePromise;\n        };\n    },\n};\n\nregistry.category(\"services\").add(\"get_color_picker_template\", getColorPickerTemplateService);\n", "/** @odoo-module **/\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", "/** @odoo-module **/\n\nimport { rpc } from \"@web/core/network/rpc\";\nimport { pick } from \"@web/core/utils/objects\";\nimport {getAffineApproximation, getProjective} from \"@web_editor/js/editor/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'];\nconst modifierFields = [\n    'filter',\n    'quality',\n    'mimetype',\n    'glFilter',\n    'originalId',\n    'originalSrc',\n    'resizeWidth',\n    'aspectRatio',\n    \"bgSrc\",\n    \"mimetypeBeforeConversion\",\n];\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', .3],\n            ['brightness', .1],\n            ['contrast', .1],\n            ['saturation', .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', .2],\n            ['brightness', .2],\n            ['contrast', -.1],\n            ['saturation', -.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', .31],\n            ['sepia', .5],\n            ['contrast', .4],\n        ]);\n    },\n\n    earlybird: (filter, cv) => {\n        const ctx = cv.getContext('2d');\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2, cv.height / 2, 0,\n            cv.width / 2, cv.height / 2, Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(.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', .2],\n            ['sepia', .2],\n            ['contrast', -.1],\n        ]);\n    },\n\n    inkwell: (filter, cv) => {\n        applyAll(filter, [\n            ['sepia', .3],\n            ['brightness', .1],\n            ['contrast', -.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', .25],\n            ['brightness', -.05],\n            ['contrast', -.05],\n            ['saturation', .5],\n        ]);\n    },\n\n    toaster: (filter, cv) => {\n        const ctx = cv.getContext('2d');\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2, cv.height / 2, 0,\n            cv.width / 2, cv.height / 2, 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', .5],\n            ['brightness', -.1],\n            ['contrast', .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', .3],\n            ['sepia', .3],\n            ['brightness', .1],\n            ['saturation', .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', .5],\n            ['sepia', .08],\n            ['brightness', .08],\n            ['contrast', .08],\n        ]);\n    },\n\n    xpro: (filter, cv) => {\n        const ctx = cv.getContext('2d');\n        const gradient = ctx.createRadialGradient(\n            cv.width / 2, cv.height / 2, 0,\n            cv.width / 2, cv.height / 2, Math.hypot(cv.width, cv.height) / 2\n        );\n        gradient.addColorStop(.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', .7],\n            ['sepia', .3],\n        ]);\n    },\n\n    custom: (filter, cv, filterOptions) => {\n        const options = Object.assign({\n            blend: 'normal',\n            filterColor: '',\n            blur: '0',\n            desaturateLuminance: '0',\n            saturation: '0',\n            contrast: '0',\n            brightness: '0',\n            sepia: '0',\n        }, JSON.parse(filterOptions || \"{}\"));\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(...Object.entries(options).map(([filter, amount]) => [filter, parseInt(amount) / 100]));\n        applyAll(filter, filters);\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 * @returns {string} dataURL of the image with the applied modifications\n */\nexport async function applyModifications(img, dataOptions = {}) {\n    const data = Object.assign({\n        glFilter: '',\n        filter: '#0000',\n        quality: '75',\n        forceModification: false,\n    }, img.dataset, dataOptions);\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    await activateCropper(original, 0, data);\n    let croppedImg = $(original).cropper('getCroppedCanvas', {width, height});\n    $(original).cropper('destroy');\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        await activateCropper(croppedImg, imgAspectRatio, {y: 0});\n        croppedImg = $(croppedImg).cropper('getCroppedCanvas');\n        $(croppedImg).cropper('destroy');\n    }\n\n    // Width\n    const result = document.createElement('canvas');\n    result.width = resizeWidth || croppedImg.width;\n    result.height = perspective ? result.width / svgAspectRatio : 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, 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 = {origin: [i * dx, j * dy], sides: [dx, dy], flange: 0.1, overlap: 0};\n                const lower = {origin: [i * dx + dx, j * dy + dy], sides: [-dx, -dy], flange: 0, overlap: 0.1};\n\n                for (let {origin, sides, flange, overlap} of [upper, lower]) {\n                    const [[a, c, e], [b, d, f]] = getAffineApproximation(project, [\n                        origin, [origin[0] + sides[0], origin[1]], [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(croppedImg, 0, 0, croppedImg.width, croppedImg.height, 0, 0, result.width, result.height);\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(filtered, 0, 0, filtered.width, filtered.height, 0, 0, result.width, result.height);\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 = !!perspective || !!glFilter ||\n        original.width !== result.width || original.height !== result.height ||\n        original.width !== croppedImg.width || 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 * 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 * 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 * @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 * 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 * 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    $(image).cropper({\n        viewMode: 2,\n        dragMode: 'move',\n        autoCropArea: 1.0,\n        aspectRatio: aspectRatio,\n        data: Object.fromEntries(Object.entries(pick(dataset, ...cropperDataFields))\n            .map(([key, value]) => [key, parseFloat(value)])),\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 new Promise(resolve => image.addEventListener('ready', resolve, {once: true}));\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 === \"about:srcdoc\") {\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('/web_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 (original && original.image_src && !/\\/web\\/image\\/\\d+-redirect\\//.test(original.image_src)) {\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 {String} mimetype\n * @param {Boolean} [strict=false] if true, even partially supported images (GIFs)\n *     won't be accepted.\n * @returns {Boolean}\n */\nexport function isImageSupportedForProcessing(mimetype, strict = false) {\n    if (isGif(mimetype)) {\n        return !strict;\n    }\n    return ['image/jpeg', 'image/png', 'image/webp'].includes(mimetype);\n}\n/**\n * @param {HTMLImageElement} img\n * @returns {Boolean}\n */\nexport function isImageSupportedForStyle(img) {\n    if (!img.parentElement) {\n        return false;\n    }\n\n    // See also `[data-oe-type='image'] > img` added as data-exclude of some\n    // snippet options.\n    const isTFieldImg = ('oeType' in img.parentElement.dataset);\n\n    // Editable root elements are technically *potentially* supported here (if\n    // the edited attributes are not computed inside the related view, they\n    // could technically be saved... but as we cannot tell the computed ones\n    // apart from the \"static\" ones, we choose to not support edition at all in\n    // those \"root\" cases).\n    // See also `[data-oe-xpath]` added as data-exclude of some snippet options.\n    const isEditableRootElement = ('oeXpath' in img.dataset);\n\n    return !isTFieldImg && !isEditableRootElement;\n}\n\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\nexport const removeOnImageChangeAttrs = [...cropperDataFields, ...modifierFields];\n\nexport default {\n    applyModifications,\n    cropperDataFields,\n    activateCropper,\n    loadImageInfo,\n    loadImage,\n    removeOnImageChangeAttrs,\n    isImageSupportedForProcessing,\n    isImageSupportedForStyle,\n    createDataURL,\n    isGif,\n    getDataURLBinarySize,\n};\n", "\n/** @odoo-module **/\n\n// These colors are already normalized as per normalizeCSSColor in @web/legacy/js/widgets/colorpicker\nexport default [\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", "/** @odoo-module **/\n\nimport { Component, useRef, onMounted } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\n\nexport class AltDialog extends Component {\n    static components = { Dialog };\n    static props = {\n        confirm: Function,\n        close: Function,\n        alt: String,\n        tag_title: String,\n    };\n    static template = 'web_editor.AltDialog';\n    altRef = useRef(\"alt\");\n    tagTitleRef = useRef(\"tag_title\");\n\n    setup() {\n        this.isConfirmedOrCancelled = false; // ensures we do not confirm and/or cancel twice\n        onMounted(() => {\n            this.altRef.el.focus();\n        });\n    }\n    async _cancel() {\n        if (this.isConfirmedOrCancelled) {\n            return;\n        }\n        this.isConfirmedOrCancelled = true;\n        this.props.close();\n    }\n    async _confirm() {\n        if (this.isConfirmedOrCancelled) {\n            return;\n        }\n        this.isConfirmedOrCancelled = true;\n        try {\n            const allNonEscQuots = /\"/g;\n            const alt = this.altRef.el.value.replace(allNonEscQuots, \"&quot;\");\n            const title = this.tagTitleRef.el.value.replace(allNonEscQuots, \"&quot;\");\n            await this.props.confirm(alt, title);\n        } catch (e) {\n            this.props.close();\n            throw e;\n        }\n        this.props.close();\n    }\n}\n", "/** @odoo-module **/\n\nimport { ChatGPTDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_dialog';\nimport { useState, status } from \"@odoo/owl\";\n\nexport class ChatGPTAlternativesDialog extends ChatGPTDialog {\n    static template = 'web_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        alternativesModes: {\n            correct: 'Correct',\n            short: 'Shorten',\n            long: 'Lengthen',\n            friendly: 'Friendly',\n            professional: 'Professional',\n            persuasive: 'Persuasive',\n        },\n        numberOfAlternatives: 3,\n    };\n\n    setup() {\n        super.setup();\n        this.state = useState({\n            ...this.state,\n            conversationHistory: [{\n                role: 'system',\n                content: 'The user wrote the following text:\\n' +\n                    '<generated_text>' + this.props.originalText + '</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            messages: [],\n            alternativesMode: '',\n            messagesInProgress: 0,\n            currentBatchId: null,\n        });\n        this._generationIndex = 0;\n        this._generateAlternatives();\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    switchAlternativesMode(ev) {\n        this.state.alternativesMode = ev.currentTarget.getAttribute('data-mode');\n        this._generateAlternatives(1);\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    async _generateAlternatives(numberOfAlternatives = this.props.numberOfAlternatives) {\n        this.state.messagesInProgress = numberOfAlternatives;\n        const batchId = new Date().getTime();\n        this.state.currentBatchId = batchId;\n        let wasError = false;\n        let messageIndex = 0;\n        while (!wasError && messageIndex < numberOfAlternatives && this.state.currentBatchId === batchId) {\n            this._generationIndex += 1;\n            let query = messageIndex ? 'Write one alternative version of the original text.' : '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 = '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.replace(/^[\\s\\S]*<generated_text>/, '').replace(/<\\/generated_text>[\\s\\S]*$/, '');\n                    if (isError) {\n                        wasError = true;\n                    } else {\n                        this.state.conversationHistory.push({\n                            role: 'user',\n                            content: query,\n                        }, {\n                            role: 'assistant',\n                            content,\n                        });\n                    }\n                    this.state.messages.push({\n                        author: 'assistant',\n                        text: alternative,\n                        isError,\n                        batchId,\n                        mode: this.state.alternativesMode,\n                        id: new Date().getTime(),\n                    });\n                }\n            }).catch(() => {\n                if (this.state.currentBatchId === batchId) {\n                    wasError = true;\n                    this.state.messages = [];\n                }\n            });\n            if (status(this) === 'destroyed') {\n                return;\n            }\n            messageIndex += 1;\n            this.state.messagesInProgress -= 1;\n            if (wasError) {\n                break;\n            }\n        }\n        this.state.messagesInProgress = 0;\n    }\n}\n", "/** @odoo-module **/\n\nimport { Component, useState, markup, onWillDestroy, status } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { _t } from \"@web/core/l10n/translation\";\n\n/**\n * General component for common logic between different dialogs.\n */\nexport class ChatGPTDialog extends Component {\n    static template = \"\";\n    static components = { Dialog };\n    static props = {\n        close: Function,\n        insert: Function,\n    };\n\n    setup() {\n        this.state = useState({ selectedMessageId: null });\n        onWillDestroy(() => this.pendingRpcPromise?.abort());\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    selectMessage(ev) {\n        this.state.selectedMessageId = +ev.currentTarget.getAttribute('data-message-id');\n    }\n    insertMessage(ev) {\n        this.selectMessage(ev);\n        this._confirm();\n    }\n    formatContent(content) {\n        return markup([...this._postprocessGeneratedContent(content).childNodes].map(child => {\n            // Escape all text.\n            const nodes = new Set([...child.querySelectorAll('*')].flatMap(node => node.childNodes));\n            nodes.forEach(node => {\n                if (node.nodeType === Node.TEXT_NODE) {\n                    node.textContent = escape(node.textContent);\n                }\n            });\n            return child.outerHTML;\n        }).join(''));\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _postprocessGeneratedContent(content) {\n        const lines = content.split('\\n').filter(line => line.trim().length);\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 {\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' : 'p');\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    _cancel() {\n        this.props.close();\n    }\n    _confirm() {\n        try {\n            this.props.close();\n            const text = this.state.messages.find(message => message.id === this.state.selectedMessageId)?.text;\n            this.props.insert(this._postprocessGeneratedContent(text || ''));\n        } catch (e) {\n            this.props.close();\n            throw e;\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('/web_editor/generate_text', {\n            prompt,\n            conversation_history: this.state.conversationHistory,\n        }, { shadow: true });\n        return this.pendingRpcPromise\n            .then(content => protectedCallback(content))\n            .catch(error => protectedCallback(_t(error.data?.message || error.message), true));\n    }\n}\n", "/** @odoo-module **/\n\nimport { ChatGPTDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_dialog';\nimport { useState, useEffect, useRef } from \"@odoo/owl\";\nimport { useAutofocus, useChildRef } from \"@web/core/utils/hooks\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { user } from \"@web/core/user\";\nimport { scrollTo } from \"@web/core/utils/scrolling\";\n\nexport class ChatGPTPromptDialog extends ChatGPTDialog {\n    static template = 'web_editor.ChatGPTPromptDialog';\n    static props = {\n        ...super.props,\n        initialPrompt: { type: String, optional: true },\n    };\n    static 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 = `${browser.location.origin}/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                role: 'system',\n                content: '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            messages: [],\n        });\n        this.promptInputRef = useRef('promptInput');\n        this.modalRef = useChildRef();\n        useAutofocus({ refName: 'promptInput' });\n        useEffect(() => {\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        }, () => [this.state.prompt]);\n        useEffect(() => {\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        }, () => [this.state.conversationHistory.length]);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\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    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(c => c !== conversation);\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    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _freezeInput() {\n        this.promptInputRef.el.setAttribute('disabled', '');\n    }\n    _unfreezeInput() {\n        this.promptInputRef.el.removeAttribute('disabled');\n        this.promptInputRef.el.focus();\n    }\n    /**\n     * @override\n     */\n    _cancel() {\n        this._freezeInput();\n        super._cancel();\n    }\n    /**\n     * @override\n     */\n     _confirm() {\n        this._freezeInput();\n        super._confirm();\n    }\n}\n", "import { useState } from \"@odoo/owl\";\nimport { ChatGPTDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_dialog';\n\nexport class ChatGPTTranslateDialog extends ChatGPTDialog {\n    static template = 'web_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                role: 'system',\n                content: '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            messages: [],\n            translationInProgress: true,\n        });\n        this._translate();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    async _translate() {\n        const prompt = `Translate <generated_text>${this.props.originalText}</generated_text> to ${this.props.language}`;\n        const messageId = new Date().getTime();\n        const conversation = { role: 'user', content: prompt };\n        this.state.conversationHistory.push(conversation);\n        this._generate(prompt, (content, isError) => {\n            let translatedText = content.replace(/^[\\s\\S]*<generated_text>/, '').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({ role: 'assistant', content });\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", "/** @odoo-module **/\n\nimport { Colorpicker } from \"@web/core/colorpicker/colorpicker\";\nimport customColors from \"@web_editor/js/editor/custom_colors\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport {\n    isCSSColor,\n    normalizeCSSColor,\n    convertCSSColorToRgba,\n} from '@web/core/utils/colors';\nimport {\n    Component,\n    useRef,\n    useState,\n    onWillStart,\n    onMounted,\n    onWillUpdateProps,\n} from \"@odoo/owl\";\n\nexport class ColorPalette extends Component {\n    static template = 'web_editor.ColorPalette';\n    static props = {\n        document: { type: true, optional: true },\n        resetTabCount: { type: Number, optional: true },\n        selectedCC: { type: String, optional: true },\n        selectedColor: { type: String, optional: true },\n        resetButton: { type: Boolean, optional: true },\n        excluded: { type: Array, optional: true },\n        excludeSectionOf: { type: Array, optional: true },\n        withCombinations: { type: Boolean, optional: true },\n        noTransparency: { type: Boolean, optional: true },\n        opacity: { type: Number, optional: true },\n        selectedTab: { type: String, optional: true },\n        withGradients: { type: Boolean, optional: true },\n        getTemplate: { type: Function,  optional: true },\n        onSetColorNames: { type: Function, optional: true },\n        onColorHover: { type: Function, optional: true },\n        onColorPicked: { type: Function, optional: true },\n        onCustomColorPicked: { type: Function, optional: true },\n        onColorLeave: { type: Function, optional: true },\n        onInputEnter: { type: Function, optional: true },\n        getCustomColors: { type: Function, optional: true },\n        getEditableCustomColors: { type: Function, optional: true },\n        onColorpaletteTabChange: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        document: window.document,\n        resetTabCount: 0,\n        resetButton: true,\n        excluded: [],\n        excludeSectionOf: null,\n        withCombinations: false,\n        noTransparency: false,\n        opacity: 1,\n        selectedTab: 'theme-colors',\n        withGradients: false,\n        onSetColorNames: () => {},\n        onColorHover: () => {},\n        onColorPicked: () => {},\n        onCustomColorPicked: () => {},\n        onColorLeave: () => {},\n        onInputEnter: () => {},\n        getCustomColors: () => [],\n        getEditableCustomColors: () => [],\n        onColorpaletteTabChange: () => {},\n    }\n    static components = { Colorpicker };\n    elRef = useRef('el');\n    state = useState({\n        showGradientPicker: false,\n    });\n    setup() {\n        this.init();\n        onWillStart(async () => {\n            if (this.props.getTemplate) {\n                this.colorpickerTemplate = await this.props.getTemplate();\n            }\n        });\n        onMounted(async () => {\n            if (!this.elRef.el) {\n                // There is legacy code that can trigger the instantiation of the\n                // link tool when one of it's parent component is not in the dom. If\n                // that parent element is not in the dom, owl will not return\n                // `this.linkComponentWrapperRef.el` because of a check (see\n                // `inOwnerDocument`).\n                // Todo: this workaround should be removed when the snippet menu is\n                // converted to owl.\n                await new Promise(resolve => {\n                    const observer = new MutationObserver(() => {\n                        if (this.elRef.el) {\n                            observer.disconnect();\n                            resolve();\n                        }\n                    });\n                    observer.observe(document.body, { childList: true, subtree: true });\n                });\n            }\n            this.el = this.elRef.el;\n            const $el = $(this.el);\n            this.$ = $el.find.bind($el);\n\n            $el.on('click', '.o_we_color_btn', this._onColorButtonClick.bind(this));\n            $el.on('mouseenter', '.o_we_color_btn', this._onColorButtonEnter.bind(this));\n            $el.on('mouseleave', '.o_we_color_btn', this._onColorButtonLeave.bind(this));\n\n            $el.on('click', '.o_custom_gradient_editor .o_custom_gradient_btn', this._onGradientCustomButtonClick.bind(this));\n            $el.on('click', '.o_custom_gradient_editor', this._onPanelClick.bind(this));\n            $el.on('change', '.o_custom_gradient_editor input[type=\"text\"]', this._onGradientInputChange.bind(this));\n            $el.on('keypress', '.o_custom_gradient_editor input[type=\"text\"]', this._onGradientInputKeyPress.bind(this));\n            $el.on('click', '.o_custom_gradient_editor we-button:not(.o_remove_color)', this._onGradientButtonClick.bind(this));\n            $el.on('mouseenter', '.o_custom_gradient_editor we-button:not(.o_remove_color)', this._onGradientButtonEnter.bind(this));\n            $el.on('mouseleave', '.o_custom_gradient_editor we-button:not(.o_remove_color)', this._onGradientButtonLeave.bind(this));\n\n            $el.on('click', '.o_custom_gradient_scale', this._onGradientPreviewClick.bind(this));\n            // Note: _onGradientSliderClick on slider is attached at slider creation.\n            $el.on('click', '.o_custom_gradient_editor .o_remove_color', this._onGradientDeleteClick.bind(this));\n\n            await this.start();\n        });\n        onWillUpdateProps((newProps) => {\n            this._updateColorToColornames();\n            if (this.props.resetTabCount !== newProps.resetTabCount) {\n                this._selectDefaultTab();\n            }\n            if (this.props.selectedCC !== newProps.selectedCC || this.props.selectedColor !== newProps.selectedColor) {\n                this._selectColor({\n                    ccValue: newProps.selectedCC,\n                    color: newProps.selectedColor,\n                });\n            }\n            this._buildCustomColors();\n            this._markSelectedColor();\n        });\n    }\n    init() {\n        const editableDocument = this.props.document;\n        this.style = editableDocument.defaultView.getComputedStyle(editableDocument.documentElement);\n        this.selectedColor = '';\n        this.resetButton = this.props.resetButton;\n        this.withCombinations = this.props.withCombinations;\n        this.selectedTab = this.props.selectedTab;\n\n        this.tabs = [{\n            id: 'theme-colors',\n            pickers: [\n                'theme',\n                'common',\n            ],\n        },\n        {\n            id: 'custom-colors',\n            pickers: [\n                'custom',\n                'transparent_grayscale',\n                'common_grays',\n            ],\n        },\n        {\n            id: 'gradients',\n            pickers: this.props.withGradients ? [\n                'predefined_gradients',\n                'custom_gradient',\n            ] : [],\n        }];\n\n        this.sections = {};\n        this.pickers = {};\n    }\n    /**\n     * @override\n     */\n    async start() {\n        const switchPaneButtons = this.el.querySelectorAll('.o_we_colorpicker_switch_pane_btn');\n\n        let colorpickerEl;\n        if (this.colorpickerTemplate) {\n            colorpickerEl = $(this.colorpickerTemplate)[0];\n        } else {\n            colorpickerEl = document.createElement(\"colorpicker\");\n            const sectionEl = document.createElement('DIV');\n            sectionEl.classList.add('o_colorpicker_section');\n            sectionEl.dataset.name = 'common';\n            colorpickerEl.appendChild(sectionEl);\n        }\n        colorpickerEl.querySelectorAll('button').forEach(el => el.classList.add('o_we_color_btn'));\n\n        // Populate tabs based on the tabs configuration indicated in this.tabs\n        this.tabs.forEach((tab, index) => {\n            // Append pickers to section\n            let sectionEl = this.el.querySelector(`.o_colorpicker_sections[data-color-tab=\"${tab.id}\"]`);\n            const container = sectionEl.querySelector('.o_colorpicker_section_container');\n            if (container) {\n                sectionEl = container;\n            }\n            let sectionIsEmpty = true;\n            tab.pickers.forEach((pickerId) => {\n                let pickerEl;\n                switch (pickerId) {\n                    case 'common_grays':\n                        pickerEl = colorpickerEl.querySelector('[data-name=\"common\"]').cloneNode(true);\n                        break;\n                    case 'custom':\n                        pickerEl = document.createElement('DIV');\n                        pickerEl.classList.add(\"o_colorpicker_section\");\n                        pickerEl.dataset.name = 'custom';\n                        break;\n                    default:\n                        pickerEl = colorpickerEl.querySelector(`[data-name=\"${pickerId}\"]`);\n                        pickerEl = pickerEl && pickerEl.cloneNode(true);\n                }\n                if (pickerEl) {\n                    sectionEl.appendChild(pickerEl);\n\n                    if (!this.props.excluded.includes(pickerId)) {\n                        sectionIsEmpty = false;\n                    }\n\n                    this.pickers[pickerId] = pickerEl;\n                }\n            });\n\n            // If the section is empty, hide it and\n            // select the next tab if none is given in the options\n            if (sectionIsEmpty) {\n                sectionEl.classList.add('d-none');\n                switchPaneButtons[index].classList.add('d-none');\n                if (this.selectedTab === tab.id) {\n                    this.selectedTab = this.tabs[(index + 1) % this.tabs.length].id;\n                }\n            }\n            this.sections[tab.id] = sectionEl;\n        });\n\n        // Predefined gradient opacity\n        if (this.props.withGradients && this.props.opacity !== 1) {\n            this.pickers['predefined_gradients'].querySelectorAll('button').forEach(elem => {\n                let gradient = elem.dataset.color;\n                gradient = gradient.replaceAll(/rgba?(\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+)(?:\\s*,.+?)?\\)/g,\n                    `rgba$1, ${this.props.opacity})`);\n                elem.dataset.color = gradient.replaceAll(/\\s+/g, '');\n            });\n        }\n\n        // Palette for gradient\n        if (this.pickers['custom_gradient']) {\n            const editor = this.pickers['custom_gradient'];\n            this.gradientEditorParts = {\n                'customButton': editor.querySelector('.o_custom_gradient_btn'),\n                'customContent': editor.querySelector('.o_color_picker_inputs'),\n                'linearButton': editor.querySelector('we-button[data-gradient-type=\"linear-gradient\"]'),\n                'angleRow': editor.querySelector('.o_angle_row'),\n                'angle': editor.querySelector('input[data-name=\"angle\"]'),\n                'radialButton': editor.querySelector('we-button[data-gradient-type=\"radial-gradient\"]'),\n                'positionRow': editor.querySelector('.o_position_row'),\n                'positionX': editor.querySelector('input[data-name=\"positionX\"]'),\n                'positionY': editor.querySelector('input[data-name=\"positionY\"]'),\n                'sizeRow': editor.querySelector('.o_size_row'),\n                'scale': editor.querySelector('.o_custom_gradient_scale div'),\n                'sliders': editor.querySelector('.o_slider_multi'),\n                'deleteButton': editor.querySelector('.o_remove_color'),\n            };\n            const gradient = weUtils.isColorGradient(this.props.selectedColor) && this.props.selectedColor;\n            this._selectGradient(gradient);\n            const resizeObserver = new window.ResizeObserver(() => {\n                this._adjustActiveSliderDelete();\n            });\n            resizeObserver.observe(this.gradientEditorParts.sliders);\n        }\n\n        // Switch to the correct tab\n        const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.selectedTab);\n        this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]);\n\n        // Remove the buttons display if there is only one\n        const visibleButtons = Array.from(switchPaneButtons).filter(button => !button.classList.contains('d-none'));\n        if (visibleButtons.length === 1) {\n            visibleButtons[0].classList.add('d-none');\n        }\n\n        // Remove excluded palettes (note: only hide them to still be able\n        // to remove their related colors on the DOM target)\n        this.props.excluded.forEach((exc) => {\n            this.$('[data-name=\"' + exc + '\"]').addClass('d-none');\n        });\n        if (this.props.excludeSectionOf) {\n            this.$('[data-name]:has([data-color=\"' + this.props.excludeSectionOf + '\"])').addClass('d-none');\n        }\n\n        this.el.querySelectorAll('.o_colorpicker_section').forEach(elem => {\n            $(elem).prepend('<div>' + (elem.dataset.display || '') + '</div>');\n        });\n\n        // Render common colors\n        if (!this.props.excluded.includes('common')) {\n            customColors.forEach((colorRow, i) => {\n                if (i === 0) {\n                    return; // Ignore the wysiwyg gray palette and use ours\n                }\n                const $div = $('<div/>', {class: 'clearfix'}).appendTo(this.pickers['common']);\n                colorRow.forEach(color => {\n                    $div.append(this._createColorButton(color, ['o_common_color']));\n                });\n            });\n        }\n\n        // Compute class colors\n\n        this.colorNames = [...weUtils.COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES];\n        this._updateColorToColornames();\n        this.props.onSetColorNames([...this.colorNames]);\n\n        // Select selected Color and build customColors.\n        // If no color is selected selectedColor is an empty string (transparent is interpreted as no color)\n        if (this.props.selectedCC) {\n            this.selectedCC = this.props.selectedCC;\n        }\n        this._setSelectedColor(this.props.selectedColor);\n        this._buildCustomColors();\n        this._markSelectedColor();\n\n        // Colorpicker\n        if (!this.props.excluded.includes('custom')) {\n            let defaultColor = this.selectedColor;\n            if (defaultColor && !isCSSColor(defaultColor)) {\n                defaultColor = weUtils.getCSSVariableValue(defaultColor, this.style);\n            }\n            if (!defaultColor && this.props.opacity !== 1) {\n                defaultColor = 'rgba(0, 0, 0, ' + this.props.opacity + ')';\n            }\n            this.state.customDefaultColor = defaultColor;\n        }\n    }\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Gets the currently selected colors.\n     *\n     * @private\n     * @returns {Object} ccValue and color (plain color or gradient).\n     */\n    _getSelectedColors() {\n        return {\n            ccValue: this.selectedCC,\n            color: this.selectedColor,\n        };\n    }\n    /**\n     * @private\n     */\n    _setSelectedColor(color) {\n        if (color) {\n            if (color === 'rgba(0, 0, 0, 0)' && this.props.opacity !== 1) {\n                color = 'rgba(0, 0, 0, ' + this.props.opacity + ')';\n            }\n            let selectedColor = color;\n            if (weUtils.COLOR_PALETTE_COMPATIBILITY_COLOR_NAMES.includes(selectedColor)) {\n                selectedColor = weUtils.getCSSVariableValue(selectedColor, this.style) || selectedColor;\n            }\n            selectedColor = normalizeCSSColor(selectedColor);\n            if (selectedColor !== 'rgba(0, 0, 0, 0)') {\n                this.selectedColor = this.colorToColorNames[selectedColor] || selectedColor;\n            }\n        }\n    }\n    /**\n     * @private\n     */\n    _buildCustomColors() {\n        if (this.props.excluded.includes('custom')) {\n            return;\n        }\n        this.el.querySelectorAll('.o_custom_color').forEach(el => el.remove());\n        const existingColors = new Set(Object.keys(this.colorToColorNames));\n        for (const color of this.props.getCustomColors()) {\n            this._addCustomColor(existingColors, color);\n        }\n        weUtils.getCSSVariableValue('custom-colors', this.style).split(' ').forEach(v => {\n            const color = weUtils.getCSSVariableValue(v.substring(1, v.length - 1), this.style);\n            if (isCSSColor(color)) {\n                this._addCustomColor(existingColors, color);\n            }\n        });\n        for (const color of this.props.getEditableCustomColors()) {\n            this._addCustomColor(existingColors, color);\n        }\n        if (this.selectedColor) {\n            this._addCustomColor(existingColors, this.selectedColor);\n        }\n    }\n    /**\n     * Add the color to the custom color section if it is not in the existingColors.\n     *\n     * @param {string[]} existingColors Colors currently in the colorpicker\n     * @param {string} color Color to add to the cuustom colors\n     */\n    _addCustomColor(existingColors, color) {\n        if (!color) {\n            return;\n        }\n        if (!isCSSColor(color)) {\n            color = weUtils.getCSSVariableValue(color, this.style);\n        }\n        const normColor = normalizeCSSColor(color);\n        if (!existingColors.has(normColor)) {\n            this._addCustomColorButton(normColor);\n            existingColors.add(normColor);\n        }\n    }\n    /**\n     * Add a custom button in the coresponding section.\n     *\n     * @private\n     * @param {string} color\n     * @param {string[]} classes - classes added to the button\n     * @returns {jQuery}\n     */\n    _addCustomColorButton(color, classes = []) {\n        classes.push('o_custom_color');\n        const $button = this._createColorButton(color, classes);\n        return $button.appendTo(this.pickers['custom']);\n    }\n    /**\n     * Return a color button.\n     *\n     * @param {string} color\n     * @param {string[]} classes - classes added to the button\n     * @returns {jQuery}\n     */\n    _createColorButton(color, classes) {\n        return $('<button/>', {\n            class: 'o_we_color_btn ' + classes.join(' '),\n            style: 'background-color:' + color + ';',\n        });\n    }\n    /**\n     * Gets normalized information about a color button.\n     *\n     * @private\n     * @param {HTMLElement} buttonEl\n     * @returns {Object}\n     */\n    _getButtonInfo(buttonEl) {\n        const bgColor = buttonEl.style.backgroundColor;\n        const value = buttonEl.dataset.color || (bgColor && bgColor !== 'initial' ? normalizeCSSColor(bgColor) : '') || '';\n        const info = {\n            target: buttonEl,\n        };\n        if (!value) {\n            info.ccValue = '';\n            info.color = '';\n        } else if (weUtils.isColorCombinationName(value)) {\n            info.ccValue = value;\n        } else {\n            info.color = value;\n        }\n        return info;\n    }\n    /**\n     * Set the selectedColor and trigger an event\n     *\n     * @param {Object} colorInfo\n     * @param {string} [colorInfo.ccValue]\n     * @param {string} [colorInfo.color]\n     * @param {Function} [eventCallback]\n     */\n    _selectColor(colorInfo, eventCallback) {\n        this.selectedCC = colorInfo.ccValue;\n        this.selectedColor = colorInfo.color = this.colorToColorNames[colorInfo.color] || colorInfo.color;\n        if (eventCallback) {\n            eventCallback(colorInfo);\n        }\n        this._buildCustomColors();\n        this.state.customSelectedColor = colorInfo.color;\n        const customGradient = weUtils.isColorGradient(colorInfo.color) ? colorInfo.color : false;\n        if (this.pickers['custom_gradient']) {\n            this._selectGradient(customGradient);\n        }\n        this._markSelectedColor();\n    }\n    /**\n     * Populates the gradient editor.\n     *\n     * @private\n     * @param {string} gradient CSS string\n     */\n    _selectGradient(gradient) {\n        const editor = this.gradientEditorParts;\n        this.state.showGradientPicker = false;\n        const colorSplits = [];\n        if (gradient) {\n            const colorTesterEl = document.createElement(\"div\");\n            colorTesterEl.style.display = \"none\";\n            document.body.appendChild(colorTesterEl);\n            const colorTesterStyle = window.getComputedStyle(colorTesterEl);\n            gradient = gradient.toLowerCase();\n            // Extract colors and their positions: colors can either be in the #rrggbb format,\n            // in the rgb/rgba(...) format or as color name, positions are expected to be\n            // expressed as percentages (lengths are not supported).\n            for (const entry of gradient.matchAll(/(#[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                colorTesterEl.style.color = entry[1];\n                // Ignore unknown color.\n                if (!colorTesterEl.style.color) {\n                    continue;\n                }\n                const color = colorTesterStyle.color;\n                colorSplits.push([color, entry[2].replace('%', '')]);\n            }\n            colorTesterEl.remove();\n        }\n        // Consider unsupported gradients as not gradients.\n        if (!gradient || colorSplits.length < 2) {\n            $(editor.customContent).addClass('d-none');\n            editor.customButton.style['background-image'] = '';\n            delete editor.customButton.dataset.color;\n            return;\n        }\n        $(editor.customContent).removeClass('d-none');\n        editor.customButton.style['background-image'] = gradient;\n        editor.customButton.dataset.color = gradient;\n        // The scale display shows the gradient colors horizontally by canceling the type and angle\n        // which are before the first comma.\n        const scaleGradient = gradient.replace(/[^,]+,/, 'linear-gradient(90deg,');\n        editor.scale.style['background-image'] = scaleGradient;\n\n        const isLinear = gradient.startsWith('linear-gradient(');\n        // Keep track of last selected slider's position.\n        let lastSliderPosition;\n        const activeSlider = editor.sliders.querySelector('input.active');\n        if (activeSlider) {\n            lastSliderPosition = activeSlider.value;\n        }\n        let $lastSlider;\n        // Rebuild sliders for each color milestone of the gradient.\n        editor.sliders.replaceChildren();\n        for (const index in colorSplits) {\n            const colorSplit = colorSplits[index];\n            let color = colorSplit[0];\n            const position = colorSplit[1] || 100 * index / colorSplits.length;\n            const $slider = this._createGradientSlider(position, color);\n            if (position === lastSliderPosition) {\n                $lastSlider = $slider;\n            }\n        }\n\n        editor.deleteButton.classList.add('d-none');\n        // Update form elements related to type.\n        if (isLinear) {\n            editor.linearButton.classList.add('active');\n            editor.radialButton.classList.remove('active');\n\n            let angle = gradient.match(/([0-9]+)deg/);\n            angle = angle ? angle[1] : 0;\n            editor.angle.value = angle;\n        } else {\n            editor.linearButton.classList.remove('active');\n            editor.radialButton.classList.add('active');\n\n            const sizeMatch = gradient.match(/(closest|farthest)-(side|corner)/);\n            const size = sizeMatch ? sizeMatch[0] : 'farthest-corner';\n            const $buttons = $(editor.sizeRow).find('we-button');\n            $buttons.removeClass('active');\n            $(editor.sizeRow).find(\"we-button[data-gradient-size='\" + size + \"']\").addClass('active');\n\n            const position = gradient.match(/ at ([0-9]+)% ([0-9]+)%/) || ['', '50', '50'];\n            editor.positionX.value = position[1];\n            editor.positionY.value = position[2];\n        }\n        this._updateGradientVisibility(isLinear);\n        this._activateGradientSlider($lastSlider || $(this.pickers['custom_gradient'].querySelector('.o_slider_multi input')));\n    }\n    /**\n     * Adjusts the visibility of the gradient editor elements.\n     *\n     * @private\n     * @param {boolean} isLinear\n     */\n    _updateGradientVisibility(isLinear) {\n        const editor = this.gradientEditorParts;\n        if (isLinear) {\n            editor.angleRow.classList.remove('d-none');\n            editor.angleRow.classList.add('d-flex');\n            editor.positionRow.classList.add('d-none');\n            editor.positionRow.classList.remove('d-flex');\n            editor.sizeRow.classList.add('d-none');\n            editor.sizeRow.classList.remove('d-flex');\n        } else {\n            editor.angleRow.classList.add('d-none');\n            editor.angleRow.classList.remove('d-flex');\n            editor.positionRow.classList.remove('d-none');\n            editor.positionRow.classList.add('d-flex');\n            editor.sizeRow.classList.remove('d-none');\n            editor.sizeRow.classList.add('d-flex');\n        }\n    }\n    /**\n     * Removes the transparency from an rgba color.\n     *\n     * @private\n     * @param {string} color rgba CSS color string\n     * @returns {string} rgb CSS color string\n     */\n    _opacifyColor(color) {\n        if (color.startsWith('rgba')) {\n            return color.replace('rgba', 'rgb').replace(/,\\s*[0-9.]+\\s*\\)/, ')');\n        }\n        return color;\n    }\n    /**\n     * Creates and adds a slider for the gradient color definition.\n     *\n     * @private\n     * @param {int} position between 0 and 100\n     * @param {string} color\n     * @returns {jQuery} created slider\n     */\n    _createGradientSlider(position, color) {\n        const $slider = $('<input class=\"w-100\" type=\"range\" min=\"0\" max=\"100\"/>');\n        $slider.attr('value', position);\n        $slider.attr('data-color', color);\n        $slider.css('color', this._opacifyColor(color));\n        $slider.on('click', this._onGradientSliderClick.bind(this));\n        $slider.appendTo(this.gradientEditorParts.sliders);\n        this._sortGradientSliders();\n        return $slider;\n    }\n    /**\n     * Activates a slider of the gradient color definition.\n     *\n     * @private\n     * @param {jQuery} $slider\n     */\n    _activateGradientSlider($slider) {\n        const $sliders = $(this.gradientEditorParts.sliders).find('input');\n        $sliders.removeClass('active');\n        $slider.addClass('active');\n\n        const color = $slider.data('color');\n        this.state.showGradientPicker = true;\n        this.state.gradientSelectedColor = color;\n        this._sortGradientSliders();\n        this._adjustActiveSliderDelete();\n    }\n    /**\n     * Adjusts the position of the slider delete button.\n     *\n     * @private\n     */\n    _adjustActiveSliderDelete() {\n        const $sliders = $(this.gradientEditorParts.sliders).find('input');\n        const $activeSlider = $(this.gradientEditorParts.sliders).find('input.active');\n        if ($sliders.length > 2 && $activeSlider.length) {\n            this.gradientEditorParts.deleteButton.classList.remove('d-none');\n            const sliderWidth = $activeSlider.width();\n            const thumbWidth = 12; // TODO find a way to access it in CSS\n            const deleteWidth = $(this.gradientEditorParts.deleteButton).width();\n            const pixelOffset = (sliderWidth - thumbWidth) * $activeSlider[0].value / 100 + (thumbWidth - deleteWidth) / 2;\n            this.gradientEditorParts.deleteButton.style['margin-left'] = `${pixelOffset}px`;\n            this.gradientEditorParts.deleteButton.style['margin-right'] = `-${deleteWidth / 2}px`;\n        } else {\n            this.gradientEditorParts.deleteButton.classList.add('d-none');\n        }\n    }\n    /**\n     * Reorders the sliders of the gradient color definition by their position.\n     *\n     * @private\n     */\n    _sortGradientSliders() {\n        const $sliderInputs = $(this.gradientEditorParts.sliders).find('input');\n        for (const slider of $sliderInputs.sort((a, b) => parseInt(a.value, 10) - parseInt(b.value, 10))) {\n            this.gradientEditorParts.sliders.appendChild(slider);\n        }\n    }\n    /**\n     * Computes the customized gradient from the custom gradient editor.\n     *\n     * @private\n     * @returns {string} gradient string corresponding to the currently selected options.\n     */\n    _computeGradient() {\n        const editor = this.gradientEditorParts;\n\n        const $picker = $(this.pickers['custom_gradient']);\n\n        const colors = [];\n        for (const slider of $(editor.sliders).find('input')) {\n            const color = convertCSSColorToRgba($(slider).data('color'));\n            const colorText = color.opacity !== 100 ? `rgba(${color.red}, ${color.green}, ${color.blue}, ${color.opacity / 100})`\n                : `rgb(${color.red}, ${color.green}, ${color.blue})`;\n            const position = slider.value;\n            colors.push(`${colorText} ${position}%`);\n        }\n\n        const type = $picker.find('.o_type_row we-button.active').data('gradientType');\n        const isLinear = type === 'linear-gradient';\n        let typeParam;\n        if (isLinear) {\n            const angle = editor.angle.value || 0;\n            typeParam = `${angle}deg`;\n        } else {\n            const positionX = editor.positionX.value || 50;\n            const positionY = editor.positionY.value || 50;\n            const size = $picker.find('.o_size_row we-button.active').data('gradientSize');\n            typeParam = `circle ${size} at ${positionX}% ${positionY}%`;\n        }\n\n        return `${type}(${typeParam}, ${colors.join(', ')})`;\n    }\n    /**\n     * Computes the customized gradient from the custom gradient editor and displays it.\n     *\n     * @private\n     * @param {boolean} isPreview\n     */\n    _updateGradient(isPreview) {\n        const gradient = this._computeGradient();\n        // Avoid updating an unchanged gradient.\n        if (weUtils.areCssValuesEqual(gradient, this.selectedColor) && !isPreview) {\n            return;\n        }\n        const params = {\n            ...this._getSelectedColors(),\n            color: gradient,\n        };\n        if (isPreview) {\n            this.props.onColorHover(params);\n        } else {\n            this.props.onColorPicked(params);\n        }\n    }\n    /**\n     * Marks the selected colors.\n     *\n     * @private\n     */\n    _markSelectedColor() {\n        for (const buttonEl of this.el.querySelectorAll('button')) {\n            // TODO buttons should only be search by data-color value\n            // instead of style but seems necessary for custom colors right\n            // now...\n            const value = buttonEl.dataset.color || buttonEl.style.backgroundColor;\n            // Buttons in the theme-colors tab of the palette have\n            // no opacity, hence they should be searched by removing\n            // opacity of 0.6 (which was applied by default) from\n            // the selected color.\n            const isCommonColor = buttonEl.classList.contains('o_common_color');\n            const selectedColor = isCommonColor ? this._opacifyColor(this.selectedColor) : this.selectedColor;\n            buttonEl.classList.toggle('selected', value\n                && (this.selectedCC === value || weUtils.areCssValuesEqual(selectedColor, value)));\n        }\n    }\n\n    /**\n     * Select the default tab.\n     *\n     * @private\n     */\n    _selectDefaultTab() {\n        const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.selectedTab);\n        this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]);\n    }\n    /**\n     * Display button element as selected\n     *\n     * @private\n     * @param {HTMLElement} buttonEl\n     */\n    _selectTabFromButton(buttonEl) {\n        this.el.querySelectorAll('.o_we_colorpicker_switch_pane_btn').forEach(el => {\n            el.classList.remove('active');\n        });\n        buttonEl.classList.add('active');\n        this.el.querySelectorAll('.o_colorpicker_sections').forEach(el => {\n            el.classList.toggle('d-none', el.dataset.colorTab !== buttonEl.dataset.target);\n        });\n        this.props.onColorpaletteTabChange(buttonEl.dataset.target);\n    }\n    /**\n     * Updates a gradient color from a selection in the color picker.\n     *\n     * @private\n     * @param {String} colorInfo.cssColor\n     * @param {Boolean} isPreview\n     */\n    _updateGradientColor(colorInfo, isPreview) {\n        const $slider = $(this.gradientEditorParts.sliders).find('input.active');\n        if (!weUtils.areCssValuesEqual(colorInfo.cssColor, $slider.data('color'))) {\n            const previousColor = $slider.data('color');\n            $slider.data('color', colorInfo.cssColor);\n            this._updateGradient(isPreview);\n            if (isPreview) {\n                $slider.data('color', previousColor);\n            }\n        }\n    }\n    /**\n     * @private\n     */\n    _updateColorToColornames() {\n        this.colorToColorNames = {};\n        this.el.querySelectorAll('button[data-color]:not(.o_custom_gradient_btn)').forEach(elem => {\n            const colorName = elem.dataset.color;\n            if (weUtils.isColorGradient(colorName)) {\n                return;\n            }\n            const $color = $(elem);\n            const isCCName = weUtils.isColorCombinationName(colorName);\n            if (isCCName) {\n                $color.find('.o_we_cc_preview_wrapper').addClass(`o_cc o_cc${colorName}`);\n            } else if (weUtils.EDITOR_COLOR_CSS_VARIABLES.includes(colorName)) {\n                elem.style.backgroundColor = `var(--we-cp-${colorName})`;\n            } else {\n                elem.classList.add(`bg-${colorName}`);\n            }\n            this.colorNames.push(colorName);\n            if (!isCCName && !elem.classList.contains('d-none')) {\n                const color = weUtils.getCSSVariableValue(colorName, this.style);\n                this.colorToColorNames[color] = colorName;\n            }\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a color button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorButtonClick(ev) {\n        const buttonEl = ev.currentTarget;\n        const colorInfo = {\n            ...this._getSelectedColors(),\n            ...this._getButtonInfo(buttonEl)\n        };\n        this._selectColor(colorInfo, this.props.onColorPicked);\n    }\n    /**\n     * Called when a color button is entered.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorButtonEnter(ev) {\n        this.props.onColorHover({\n            ...this._getSelectedColors(),\n            ...this._getButtonInfo(ev.currentTarget)\n        });\n    }\n    /**\n     * Called when a color button is left the data color is the color currently selected.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorButtonLeave(ev) {\n        this.props.onColorLeave({\n            ...this._getSelectedColors(),\n            target: ev.target,\n        });\n    }\n    /**\n     * Called when an update is made on the colorpicker.\n     *\n     * @private\n     * @param {Object} colorInfo\n     */\n    _onColorPickerPreview(colorInfo) {\n        this.props.onColorHover({\n            ...this._getSelectedColors(),\n            color: colorInfo.cssColor,\n        });\n    }\n    /**\n     * Called when an update is made on the gradient colorpicker.\n     *\n     * @private\n     * @param {Object} colorInfo\n     */\n    _onColorPickerPreviewGradient(colorInfo) {\n        this._updateGradientColor(colorInfo, true);\n    }\n    /**\n     * Called when a color is selected on the colorpicker (mouseup).\n     *\n     * @private\n     * @param {Object} colorInfo\n     */\n    _onColorPickerSelect(colorInfo) {\n        this._selectColor({\n            ...this._getSelectedColors(),\n            color: colorInfo.cssColor,\n        }, this.props.onCustomColorPicked);\n    }\n    /**\n     * Called when a color is selected on the gradient colorpicker (mouseup).\n     *\n     * @private\n     * @param {Object} colorInfo\n     */\n    _onColorPickerSelectGradient(colorInfo) {\n        this._updateGradientColor(colorInfo);\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSwitchPaneButtonClick(ev) {\n        ev.stopPropagation();\n        this._selectTabFromButton(ev.currentTarget);\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientSliderClick(ev) {\n        ev.stopPropagation();\n        this._activateGradientSlider($(ev.target));\n        this._updateGradient();\n    }\n    /**\n     * Adds a color inside the gradient based on the position clicked within the preview.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientPreviewClick(ev) {\n        ev.stopPropagation();\n        const offset = ev.offsetX;\n        const width = parseInt(window.getComputedStyle(ev.target).width, 10);\n        const position = 100 * offset / width;\n\n        let previousColor;\n        let nextColor;\n        let previousPosition;\n        let nextPosition;\n        for (const slider of $(this.gradientEditorParts.sliders).find('input')) {\n            if (slider.value < position) {\n                previousColor = slider.dataset.color;\n                previousPosition = slider.value;\n            } else {\n                nextColor = slider.dataset.color;\n                nextPosition = slider.value;\n                break;\n            }\n        }\n        let color;\n        if (previousColor && nextColor) {\n            previousColor = convertCSSColorToRgba(previousColor);\n            nextColor = convertCSSColorToRgba(nextColor);\n            const previousRatio = (nextPosition - position) / (nextPosition - previousPosition);\n            const nextRatio = 1 - previousRatio;\n            const red = Math.round(previousRatio * previousColor.red + nextRatio * nextColor.red);\n            const green = Math.round(previousRatio * previousColor.green + nextRatio * nextColor.green);\n            const blue = Math.round(previousRatio * previousColor.blue + nextRatio * nextColor.blue);\n            const opacity = Math.round(previousRatio * previousColor.opacity + nextRatio * nextColor.opacity);\n            color = `rgba(${red}, ${green}, ${blue}, ${opacity / 100})`;\n        } else {\n            color = nextColor || previousColor || 'rgba(128, 128, 128, 0.5)';\n        }\n\n        const $slider = this._createGradientSlider(position, color);\n        this._activateGradientSlider($slider);\n        this._updateGradient();\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onPanelClick(ev) {\n        // Ignore to avoid closing popup.\n        ev.stopPropagation();\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientInputChange(ev) {\n        this._updateGradient();\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientInputKeyPress(ev) {\n        if (ev.key === \"Enter\") {\n            ev.preventDefault();\n            this._onGradientInputChange();\n        }\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientButtonClick(ev) {\n        const $buttons = $(ev.target).closest('span').find('we-button');\n        $buttons.removeClass('active');\n        $(ev.target).closest('we-button').addClass('active');\n        this._updateGradient();\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientButtonEnter(ev) {\n        ev.stopPropagation();\n        const $activeButton = $(ev.target).closest('span').find('we-button.active');\n        const $buttons = $(ev.target).closest('span').find('we-button');\n        $buttons.removeClass('active');\n        $(ev.target).closest('we-button').addClass('active');\n        this._updateGradient(true);\n        $buttons.removeClass('active');\n        $activeButton.addClass('active');\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientButtonLeave(ev) {\n        ev.stopPropagation();\n        this.props.onColorLeave({\n            ...this._getSelectedColors(),\n            target: ev.target,\n        });\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientCustomButtonClick(ev) {\n        let gradient = this.gradientEditorParts.customButton.style['backgroundImage'];\n        if (!gradient) {\n            // default to first predefined\n            gradient = this.pickers['predefined_gradients'].querySelector('button').dataset.color;\n        }\n        this._selectColor({\n            ...this._getSelectedColors(),\n            color: gradient,\n            target: this.gradientEditorParts.customButton,\n        }, this.props.onCustomColorPicked);\n        this._updateGradient();\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onGradientDeleteClick(ev) {\n        ev.stopPropagation();\n        const $activeSlider = $(this.pickers['custom_gradient'].querySelector('.o_slider_multi input.active'));\n        $activeSlider.off();\n        $activeSlider.remove();\n        this.gradientEditorParts.deleteButton.classList.add('d-none');\n        this.gradientEditorParts.deleteButton.classList.remove('active');\n        this._updateGradient();\n        this._activateGradientSlider($(this.pickers['custom_gradient'].querySelector('.o_slider_multi input')));\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onColorpickerClick(ev) {\n        if (ev.target.matches(\".o_colorpicker_section, .o_colorpicker_sections\")) {\n            ev.stopPropagation();\n        }\n    }\n}\n", "/** @odoo-module **/\n\nimport {applyModifications, cropperDataFields, activateCropper, loadImage, loadImageInfo} from \"@web_editor/js/editor/image_processing\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    Component,\n    useRef,\n    useState,\n    onMounted,\n    onWillDestroy,\n    onWillUpdateProps,\n    markup,\n} from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { closestScrollableY } from \"@web/core/utils/scrolling\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\nimport { preserveCursor } from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\n\nexport class ImageCrop extends Component {\n    static template = 'web_editor.ImageCrop';\n    static props = {\n        showCount: { type: Number, optional: true },\n        activeOnStart: { type: Boolean, optional: true },\n        media: { optional: true },\n        mimetype: { type: String, optional: true },\n    };\n    static defaultProps = {\n        activeOnStart: false,\n        showCount: 0,\n    };\n    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    state = useState({\n        active: false,\n    });\n\n    elRef = useRef('el');\n    _cropperClosed = true;\n\n    setup() {\n        // This promise is resolved when the component is mounted. It is\n        // required by a legacy mechanism to wait for the component to be\n        // mounted. See `ImageTools.resetCrop`.\n        this.mountedPromise = new Promise((resolve) => {\n            this.mountedResolve = resolve;\n        });\n        this.notification = useService(\"notification\");\n        onMounted(async () => {\n            const $el = $(this.elRef.el);\n            this.$ = $el.find.bind($el);\n            this.$('[data-action]').on('click', this._onCropOptionClick.bind(this));\n            $el.on('zoom', this._onCropZoom.bind(this));\n            if (this.props.activeOnStart) {\n                this.state.active = true;\n                await this._show(this.props);\n            }\n            this.mountedResolve();\n        });\n        onWillUpdateProps((newProps) => {\n            if (newProps.showCount !== this.props.showCount) {\n                this.state.active = true;\n            }\n            return this._show(newProps);\n        });\n        onWillDestroy(() => {\n            this._closeCropper();\n        });\n    }\n\n    _closeCropper() {\n        if (this._cropperClosed) return;\n        this._cropperClosed = true;\n        if (this.$cropperImage) {\n            this.$cropperImage.cropper('destroy');\n            this.elRef.el.ownerDocument.removeEventListener('mousedown', this._onDocumentMousedown, {capture: true});\n            this.elRef.el.ownerDocument.removeEventListener('keydown', this._onDocumentKeydown, {capture: true});\n        }\n        this.media.setAttribute('src', this.initialSrc);\n        this.$media.trigger('image_cropper_destroyed');\n        this.state.active = false;\n        this.restoreCursor();\n    }\n\n    /**\n     * Resets the crop\n     */\n    async reset() {\n        if (this.$cropperImage) {\n            this.$cropperImage.cropper('reset');\n            if (this.aspectRatio !== '0/0') {\n                this.aspectRatio = '0/0';\n                this.$cropperImage.cropper('setAspectRatio', this.aspectRatios[this.aspectRatio].value);\n            }\n            await this._save();\n        }\n    }\n\n    /**\n     * Crops the image into a 1:1 ratio or resets the crop, depending on the\n     * preview mode.\n     *\n     *  @param {boolean} previewMode \"reset\", true or false.\n     */\n    async cropSquare(previewMode) {\n        if(previewMode === \"reset\"){\n            if (this.$cropperImage) {\n                this.$cropperImage.cropper(\"setAspectRatio\", this.aspectRatios[this.aspectRatio].value);\n                await this._save(false);\n            }\n        } else {\n            const ratio = \"1/1\";\n            if (this.$cropperImage) {\n                if (this.aspectRatio !== ratio) {\n                    this.aspectRatio = previewMode ? this.aspectRatio : ratio;\n                    this.$cropperImage.cropper(\"setAspectRatio\", this.aspectRatios[ratio].value);\n                }\n                await this._save(false);\n            }\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _show(props) {\n        if (!props.media || !this.state.active) {\n            return;\n        }\n        this._cropperClosed = false;\n        this.media = props.media;\n        this.$media = $(this.media);\n        // Needed for editors in iframes.\n        this.document = this.media.ownerDocument;\n        this.restoreCursor = preserveCursor(this.media.ownerDocument);\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 = data.mimetype ||\n                src.endsWith('.png') ? 'image/png' :\n                src.endsWith('.webp') ? '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(this.media.dataset.originalSrc);\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(_t(\"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                    title: _t(\"This image is an external image\"),\n                    type: 'warning',\n                }\n            )\n            return this._closeCropper();\n        }\n        const $cropperWrapper = this.$('.o_we_cropper_wrapper');\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        this.$cropperImage = this.$('.o_we_cropper_img');\n        const cropperImage = this.$cropperImage[0];\n        [cropperImage.style.width, cropperImage.style.height] = [this.$media.width() + 'px', this.$media.height() + 'px'];\n\n        const sel = this.document.getSelection();\n        sel && sel.removeAllRanges();\n\n        // Overlaying the cropper image over the real image\n        const mediaRect = this.media.getBoundingClientRect();\n        const offset = { left: mediaRect.left, top: mediaRect.top };\n        offset.left += parseInt(this.$media.css('padding-left'));\n        offset.top += parseInt(this.$media.css('padding-right'));\n        const frameElement = this.$media[0].ownerDocument.defaultView.frameElement\n        if (frameElement) {\n            const frameRect = frameElement.getBoundingClientRect();\n            offset.left += frameRect.left;\n            offset.top += frameRect.top;\n        }\n        $cropperWrapper[0].style.left = `${offset.left}px`;\n        $cropperWrapper[0].style.top = `${offset.top}px`;\n\n        await loadImage(this.originalSrc, cropperImage);\n\n        // We need to remove the d-none class for the cropper library to work.\n        this.elRef.el.classList.remove('d-none');\n        await activateCropper(cropperImage, this.aspectRatios[this.aspectRatio].value, this.media.dataset);\n\n        this._onDocumentMousedown = this._onDocumentMousedown.bind(this);\n        this._onDocumentKeydown = this._onDocumentKeydown.bind(this);\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        this.elRef.el.ownerDocument.addEventListener('mousedown', this._onDocumentMousedown, {capture: true});\n        this.elRef.el.ownerDocument.addEventListener('keydown', this._onDocumentKeydown, {capture: 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} [refreshOptions=true]\n     */\n    async _save(refreshOptions = 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, {forceModification: true, mimetype: this.mimetype});\n        const cropped = this.aspectRatio !== \"0/0\";\n        this.media.classList.toggle('o_we_image_cropped', cropped);\n        if(refreshOptions){\n            this.$media.trigger('image_cropped');\n        }\n        this._closeCropper();\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.$cropperImage.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.$cropperImage.cropper('clear');\n        this.$cropperImage.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                duration: 500,\n                ...(scrollable && { scrollable }),\n            });\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a crop option is clicked -> change the crop area accordingly.\n     *\n     * @private\n     * @param {MouseEvent} ev\n     */\n    _onCropOptionClick(ev) {\n        const {action, value, scaleDirection} = ev.currentTarget.dataset;\n        switch (action) {\n            case 'ratio':\n                this.$cropperImage.cropper('reset');\n                this.aspectRatio = value;\n                this.$cropperImage.cropper('setAspectRatio', this.aspectRatios[this.aspectRatio].value);\n                break;\n            case 'zoom':\n            case 'reset':\n                this.$cropperImage.cropper(action, value);\n                break;\n            case 'rotate':\n                this.$cropperImage.cropper(action, value);\n                this._resetCropBox();\n                break;\n            case 'flip': {\n                const amount = this.$cropperImage.cropper('getData')[scaleDirection] * -1;\n                return this.$cropperImage.cropper(scaleDirection, amount);\n            }\n            case 'apply':\n                return this._save();\n            case 'discard':\n                return this._closeCropper();\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 (this.elRef.el.ownerDocument.body.contains(ev.target) && this.$(ev.target).length === 0) {\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", "/** @odoo-module **/\n\nimport * as OdooEditorLib from \"@web_editor/js/editor/odoo-editor/src/OdooEditor\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { isVisible } from \"@web/core/utils/ui\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport {\n    Component,\n    onWillStart,\n    onMounted,\n    onWillUpdateProps,\n    useState,\n    useRef,\n} from \"@odoo/owl\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport { deduceURLfromText } from \"@web_editor/js/editor/odoo-editor/src/utils/sanitize\";\n\nconst { getDeepRange, getInSelection, EMAIL_REGEX, PHONE_REGEX } = OdooEditorLib;\n\n/**\n * Allows to customize link content and style.\n */\nexport class Link extends Component {\n    static template = \"\";\n    static props = {\n        editable: true,\n        link: true,\n        needLabel: { type: Boolean, optional: true },\n        forceNewWindow: { type: Boolean, optional: true },\n        initialIsNewWindow: { type: Boolean, optional: true },\n        shouldFocusUrl: { type: Boolean, optional: true },\n    };\n    static defaultProps = {\n        needLabel: true,\n        forceNewWindow: false,\n        initialIsNewWindow: false,\n        shouldFocusUrl: false,\n    }\n    linkComponentWrapperRef = useRef(\"linkComponentWrapper\");\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    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({});\n        // We need to wait for the `onMounted` changes to be done before\n        // accessing `this.$el`.\n        this.mountedPromise = new Promise(resolve => this.mountedResolve = resolve);\n\n        onWillStart(async () => {\n            await this._updateState(this.props);\n        });\n        let started = false;\n        onMounted(async () => {\n            if (started) {\n                return;\n            }\n            started = true;\n            if (!this.linkComponentWrapperRef.el) {\n                // There is legacy code that can trigger the instantiation of the\n                // link tool when it's parent component (the toolbar) is not in the\n                // dom. If the parent element is not in the dom, owl will not return\n                // `this.linkComponentWrapperRef.el` because of a check (see\n                // `inOwnerDocument`).\n                // Todo: this workaround should be removed when the snippet menu is\n                // converted to owl.\n                await new Promise(resolve => {\n                    const observer = new MutationObserver(() => {\n                        if (this.linkComponentWrapperRef.el) {\n                            observer.disconnect();\n                            resolve();\n                        }\n                    });\n                    observer.observe(document.body, { childList: true, subtree: true });\n                });\n            }\n            this.$el = $(this.linkComponentWrapperRef.el);\n\n            this.$el.find('input, select').on('input', this._onAnyChange.bind(this));\n            this.$el.find('input, select').on('change', this._onAnyChange.bind(this));\n            this.$el.find('[name=\"url\"]').on('input', this.__onURLInput.bind(this));\n            this.$el.find('[name=\"url\"]').on('change', this._onURLInputChange.bind(this));\n\n            await this.start();\n            this.mountedResolve();\n        });\n        onWillUpdateProps(this.willUpdateProps);\n    }\n    /**\n     * @override\n     */\n    async start() {\n        this._setSelectOptionFromLink();\n        this.buttonOptsCollapseEl = this.linkComponentWrapperRef.el.querySelector('#o_link_dialog_button_opts_collapse');\n        this._updateOptionsUI();\n\n        this.$el[0].querySelector('#o_link_dialog_label_input').value = this.state.originalText;\n        this._setUrl({ shouldFocus: this.props.shouldFocusUrl });\n    }\n    async willUpdateProps(newProps) {\n        await this.mountedPromise;\n        await this._updateState(newProps);\n        this.state.url = newProps.link.getAttribute(\"href\") || \"\";\n        this._setUrl({ shouldFocus: newProps.shouldFocusUrl });\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Apply the new link to the DOM (via `this.$link`).\n     *\n     * @param {object} data\n     */\n    applyLinkToDom(data) {\n        // Some mass mailing template use <a class=\"btn btn-link\"> instead of just a simple <a>.\n        // And we need to keep the classes because the a.btn.btn-link have some special css rules.\n        // Same thing for the \"btn-success\" class, this class cannot be added\n        // by the options but we still have to ensure that it is not removed if\n        // it exists in a template (e.g. \"Newsletter Block\" snippet).\n        if (!data.classes.split(' ').includes('btn')) {\n            for (const linkClass of this.toleratedClasses) {\n                if (this.state.iniClassName && this.state.iniClassName.split(' ').includes(linkClass)) {\n                    data.classes += \" btn \" + linkClass;\n                }\n            }\n        }\n        // When multiple buttons follow each other, they may break on 2 lines\n        // or more on mobile, so they need a margin-bottom.\n        if (data.classes.split(\" \").includes(\"btn\")) {\n            const closestButtonSiblingEls = this._getDirectButtonSiblings(this.linkEl);\n            if (closestButtonSiblingEls.length) {\n                data.classes += \" mb-2\";\n                closestButtonSiblingEls.forEach(btnEl => btnEl.classList.add(\"mb-2\"));\n            }\n        }\n        if (['btn-custom', 'btn-outline-custom', 'btn-fill-custom'].some(className =>\n            data.classes.includes(className)\n        )) {\n            this.$link.css('color', data.classes.includes(data.customTextColor) ? '' : data.customTextColor);\n            this.$link.css('background-color', data.classes.includes(data.customFill) || weUtils.isColorGradient(data.customFill) ? '' : data.customFill);\n            this.$link.css('background-image', weUtils.isColorGradient(data.customFill) ? data.customFill : '');\n            this.$link.css('border-width', data.customBorderWidth);\n            this.$link.css('border-style', data.customBorderStyle);\n            this.$link.css('border-color', data.customBorder);\n        } else {\n            this.$link.css('color', '');\n            this.$link.css('background-color', '');\n            this.$link.css('background-image', '');\n            this.$link.css('border-width', '');\n            this.$link.css('border-style', '');\n            this.$link.css('border-color', '');\n        }\n        const attrs = Object.assign({}, this.state.oldAttributes, {\n            href: data.url,\n            target: data.isNewWindow ? '_blank' : '',\n        });\n        if (typeof data.classes === \"string\") {\n            data.classes = data.classes.replace(/o_default_snippet_text/, '');\n            attrs.class = `${data.classes}`;\n        }\n        if (data.rel) {\n            attrs.rel = `${data.rel}`;\n        }\n\n        this.$link.attr(attrs);\n        if (!this.$link.attr('target')) {\n            this.$link[0].removeAttribute('target');\n        }\n        this._updateLinkContent(this.$link, data);\n    }\n    /**\n     * Focuses the url input.\n     */\n    focusUrl() {\n        const urlInput = this.$el[0].querySelector('input[name=\"url\"]');\n        urlInput.focus();\n        urlInput.select();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _setUrl({ shouldFocus } = {}) {\n        if (this.state.url) {\n            const protocolLessUrl = this.state.url.replace(/^(https?|mailto|tel):(\\/\\/)?/i, '');\n            this.$el.find('input[name=\"url\"]').val(protocolLessUrl);\n            this._onURLInput();\n        }\n        if (shouldFocus) {\n            this.focusUrl();\n        }\n    }\n    /**\n     * @private\n     */\n    _setSelectOptionFromLink() {\n        for (const option of this._getLinkOptions()) {\n            const $option = $(option);\n            const value = $option.is('input') ? $option.val() : $option.data('value') || option.getAttribute('value');\n            let active = false;\n            if (value) {\n                const subValues = value.split(',');\n                let subActive = true;\n                for (let subValue of subValues) {\n                    const classPrefix = new RegExp('(^|btn-| |btn-outline-|btn-fill-)' + subValue);\n                    subActive = subActive && classPrefix.test(this.state.iniClassName);\n                }\n                active = subActive;\n            } else {\n                active = !this.state.iniClassName\n                         || this.toleratedClasses.some(val => this.state.iniClassName.split(' ').includes(val))\n                         || !this.state.iniClassName.includes('btn-');\n            }\n            this._setSelectOption($option, active);\n        }\n    }\n    /**\n     * Abstract method: adapt the link to changes.\n     *\n     * @abstract\n     * @private\n     */\n    _adaptPreview() {}\n    /**\n     * @private\n     */\n    _correctLink(url) {\n        if (url.indexOf('tel:') === 0) {\n            url = url.replace(/^tel:([0-9]+)$/, 'tel://$1');\n        } else if (url && !url.startsWith('mailto:') && url.indexOf('://') === -1\n                    && url[0] !== '/' && url[0] !== '#' && url.slice(0, 2) !== '${') {\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.linkEl) || '';\n        }\n    }\n    /**\n     * Abstract method: return true if the URL should be stripped of its domain.\n     *\n     * @abstract\n     * @private\n     * @returns {boolean}\n     */\n    _doStripDomain() {}\n    /**\n     * Get the link's data (url, content and styles).\n     *\n     * @private\n     * @returns {Object} {content: String, url: String, classes: String, isNewWindow: Boolean}\n     */\n    _getData() {\n        var $url = this.$el.find('input[name=\"url\"]');\n        var url = $url.val();\n        var content = this.$el.find('input[name=\"label\"]').val() || url;\n\n        if (!this.state.isButton && $url.prop('required') && (!url || !$url[0].checkValidity())) {\n            return null;\n        }\n\n        const type = this._getLinkType();\n        const customTextColor = this._getLinkCustomTextColor();\n        const customFill = this._getLinkCustomFill();\n        const customBorder = this._getLinkCustomBorder();\n        const customBorderWidth = this._getLinkCustomBorderWidth();\n        const customBorderStyle = this._getLinkCustomBorderStyle();\n        const customClasses = this._getLinkCustomClasses();\n        const size = this._getLinkSize();\n        const shape = this._getLinkShape();\n        const shapes = shape ? shape.split(',') : [];\n        const style = ['outline', 'fill'].includes(shapes[0]) ? `${shapes[0]}-` : '';\n        const shapeClasses = shapes.slice(style ? 1 : 0).join(' ');\n        const classes = (this.state.className || '') +\n            (type ? (` btn btn-${style}${type}`) : '') +\n            (type === 'custom' ? customClasses : '') +\n            (type && shapeClasses ? (` ${shapeClasses}`) : '') +\n            (type && size ? (' btn-' + size) : '');\n        var isNewWindow = this._isNewWindow(url);\n        var doStripDomain = this._doStripDomain();\n        let urlWithoutDomain = this.state.url;\n        if (this.state.url.indexOf(location.origin) === 0) {\n            urlWithoutDomain = this.state.url.slice(location.origin.length);\n            if (doStripDomain) {\n                this.state.url = urlWithoutDomain;\n            }\n        } else if (url.indexOf(location.origin) === 0 && !doStripDomain) {\n            this.state.url = url;\n        }\n        var allWhitespace = /\\s+/gi;\n        var allStartAndEndSpace = /^\\s+|\\s+$/gi;\n        const isImage = this.props.link && this.props.link.querySelector('img');\n        let isDocument = false;\n        let directDownload = true;\n        if (urlWithoutDomain && urlWithoutDomain.startsWith(\"/web/content/\")) {\n            isDocument = !this.isLastAttachmentUrl;\n            directDownload = urlWithoutDomain.includes(\"&download=true\");\n        } \n        \n        return {\n            content: content,\n            url: this._correctLink(this.state.url),\n            classes: classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, ''),\n            customTextColor: customTextColor,\n            customFill: customFill,\n            customBorder: customBorder,\n            customBorderWidth: customBorderWidth,\n            customBorderStyle: customBorderStyle,\n            oldAttributes: this.state.oldAttributes,\n            isNewWindow: isDocument || isNewWindow,\n            doStripDomain: doStripDomain,\n            isImage,\n            isDocument,\n            directDownload: directDownload && isDocument,\n        };\n    }\n    /**\n     * Return a list of all the descendants of a given element.\n     *\n     * @private\n     * @param {Node} rootNode\n     * @returns {Node[]}\n     */\n    _getDescendants(rootNode) {\n        const nodes = [];\n        for (const node of rootNode.childNodes) {\n            nodes.push(node);\n            nodes.push(...this._getDescendants(node));\n        }\n        return nodes;\n    }\n    /**\n     * Abstract method: return a JQuery object containing the UI elements\n     * holding the \"Open in new window\" option's row of the link.\n     *\n     * @abstract\n     * @private\n     * @returns {JQuery}\n     */\n    _getIsNewWindowFormRow() {}\n    /**\n     * Abstract method: return a JQuery object containing the UI elements\n     * holding the styling options of the link (eg: color, size, shape).\n     *\n     * @abstract\n     * @private\n     * @returns {JQuery}\n     */\n    _getLinkOptions() {}\n    /**\n     * Abstract method: return the shape(s) to apply to the link (eg:\n     * \"outline\", \"rounded-circle\", \"outline,rounded-circle\").\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkShape() {}\n    /**\n     * Abstract method: return the size to apply to the link (eg:\n     * \"sm\", \"lg\").\n     *\n     * @private\n     * @returns {string}\n     */\n    _getLinkSize() {}\n    /**\n     * Abstract method: return the type to apply to the link (eg:\n     * \"primary\", \"secondary\").\n     *\n     * @private\n     * @returns {string}\n     */\n    _getLinkType() {}\n    /**\n     * Returns the custom text color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomTextColor() {}\n    /**\n     * Returns the custom border color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorder() {}\n    /**\n     * Returns the custom border width for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorderWidth() {}\n    /**\n     * Returns the custom border style for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorderStyle() {}\n    /**\n     * Returns the custom fill color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomFill() {}\n    /**\n     * Returns the custom text, fill and border color classes for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomClasses() {}\n    /**\n     * @private\n     */\n    _isFromAnotherHostName(url) {\n        if (url.includes(window.location.hostname)) {\n            return false;\n        }\n        try {\n            const Url = URL || window.URL || window.webkitURL;\n            const urlObj = url.startsWith('/') ? new Url(url, window.location.origin) : new Url(url);\n            return (urlObj.origin !== window.location.origin);\n        } catch {\n            return true;\n        }\n    }\n    /**\n     * Abstract method: return true if the link should open in a new window.\n     *\n     * @abstract\n     * @private\n     * @returns {boolean}\n     */\n    _isNewWindow(url) {}\n    /**\n     * Abstract method: mark one or several options as active or inactive.\n     *\n     * @abstract\n     * @private\n     * @param {JQuery} $option\n     * @param {boolean} [active]\n     */\n    _setSelectOption($option, active) {}\n    /**\n     * Update the link content.\n     *\n     * @private\n     * @param {JQuery} $link\n     * @param {object} linkInfos\n     * @param {boolean} force\n     */\n    _updateLinkContent($link, linkInfos, { force = false } = {}) {\n        if (force || (this.props.needLabel && (linkInfos.content !== this.state.originalText || linkInfos.url !== this.state.url))) {\n            if (linkInfos.content === this.state.originalText || linkInfos.isImage) {\n                $link.html(this.state.originalHTML.replaceAll('\\u200B', '').replaceAll('\\uFEFF', ''));\n            } else if (linkInfos.content && linkInfos.content.length) {\n                let contentWrapperEl = $link[0];\n                const text = $link[0].innerText.replaceAll(\"\\u200B\", \"\").replaceAll(\"\\uFEFF\", \"\").trim();\n                // Update the first not ZWS child element that has the same inner text\n                // as the link with the new content while preserving child\n                // elements within the link. (e.g. the link is bold and italic)\n                let child;\n                do {\n                    contentWrapperEl = child || contentWrapperEl;\n                    child = [...contentWrapperEl.children].find(\n                        (element) => !element.hasAttribute(\"data-o-link-zws\")\n                    );\n                } while (child?.innerText.replaceAll('\\u200B', '').replaceAll('\\uFEFF', '').trim() === text);\n                contentWrapperEl.innerText = linkInfos.content;\n            } else {\n                $link.text(linkInfos.url);\n            }\n        }\n    }\n    /**\n     * @abstract\n     * @private\n     */\n    _updateOptionsUI() {}\n    /**\n     * Update the state.\n     *\n     * @private\n     */\n    async _updateState(props) {\n        // TODO In master move to link_tools.\n        this.initialIsNewWindowFromProps = props.initialIsNewWindow;\n        this.initialNewWindow = props.initialIsNewWindow;\n\n        this.state.className = \"\";\n        this.state.iniClassName = \"\";\n\n        // The classes in the following array should not be in editable areas\n        // but as there are still some (e.g. in the \"newsletter block\" snippet)\n        // we make sure the options system works with them.\n        this.toleratedClasses = ['btn-link', 'btn-success'];\n\n        this.editable = props.editable;\n        this.$editable = $(this.editable);\n\n        if (props.link) {\n            const range = document.createRange();\n            range.selectNodeContents(props.link);\n            this.state.range = range;\n            this.$link = $(props.link);\n            this.linkEl = props.link;\n        }\n\n        if (this.state.range) {\n            this.$link = this.$link || $(OdooEditorLib.getInSelection(this.editable.ownerDocument, 'a'));\n            this.linkEl = this.$link[0];\n            this.state.iniClassName = this.$link.attr('class') || '';\n            this.colorCombinationClass = false;\n            let $node = this.$link;\n            while ($node.length && !$node.is('body')) {\n                const className = $node.attr('class') || '';\n                const m = className.match(/\\b(o_cc\\d+)\\b/g);\n                if (m) {\n                    this.colorCombinationClass = m[0];\n                    break;\n                }\n                $node = $node.parent();\n            }\n            const linkNode = this.$link[0] || this.state.range.cloneContents();\n            const linkText = weUtils.getLinkLabel(linkNode);\n            this.state.originalText = linkText.replace(/[ \\t\\r\\n]+/g, ' ');\n            if (linkNode instanceof DocumentFragment) {\n                this.state.originalHTML = $('<fakeEl>').append(linkNode).html();\n            } else {\n                this.state.originalHTML = linkNode.innerHTML;\n            }\n            this.state.url = this.$link.attr('href') || '';\n        } else {\n            this.state.originalText = this.state.originalText ? this.state.originalText.replace(/[ \\t\\r\\n]+/g, ' ') : '';\n        }\n\n        this.state.url ||= this._deduceUrl(this.state.originalText, this.linkEl);\n\n        if (this.linkEl) {\n            this.initialNewWindow = this.initialNewWindow || this.linkEl.target === '_blank';\n            await this._determineAttachmentType(this.linkEl.pathname);\n        }\n\n        const classesToKeep = [\n            'text-wrap', 'text-nowrap', 'text-start', 'text-center', 'text-end',\n            'text-truncate',\n        ];\n        const keptClasses = this.state.iniClassName.split(' ').filter(className => classesToKeep.includes(className));\n        const allBtnColorPrefixes = /(^|\\s+)(bg|text|border)((-[a-z0-9_-]*)|\\b)/gi;\n        const allBtnClassSuffixes = /(^|\\s+)btn((-[a-z0-9_-]*)|\\b)/gi;\n        const allBtnShapes = /\\s*(rounded-circle|flat)\\s*/gi;\n        const btnMarginBottom = /(^|\\s+)mb-2(\\s+|$)/i;\n        this.state.className = this.state.iniClassName\n            .replace(allBtnColorPrefixes, ' ')\n            .replace(allBtnClassSuffixes, ' ')\n            .replace(allBtnShapes, \" \")\n            .replace(btnMarginBottom, \" \");\n        this.state.className += ' ' + keptClasses.join(' ');\n        // 'o_submit' class will force anchor to be handled as a button in linkdialog.\n        if (/(?:s_website_form_send|o_submit)/.test(this.state.className)) {\n            this.state.isButton = true;\n        }\n    }\n    /**\n     * If the current link is an attachment: stores the attachment id in\n     * lastAttachmentId, and if not yet known fetches the type of the\n     * attachment and stores true in isLastAttachmentUrl if its type is URL.\n     */\n    async _determineAttachmentType(pathname) {\n        if (pathname?.startsWith(\"/web/content/\")) {\n            const attachmentId = parseInt(pathname.substr(\"/web/content/\".length));\n            if (this.lastAttachmentId === attachmentId) {\n                return;\n            }\n            this.lastAttachmentId = attachmentId;\n            // Find out about attachment type.\n            try {\n                const fetched = await this.orm.read(\"ir.attachment\", [attachmentId], [\"type\"]);\n                this.isLastAttachmentUrl = fetched[0].type === \"url\";\n            } catch {\n                // Not a reachable attachment\n                this.isLastAttachmentUrl = undefined;\n            }\n        }\n    }\n    /**\n     * Returns an array of the buttons which are the closest non empty\n     * previousSibling and/or nextSibling.\n     *\n     * @param {HTMLElement} el\n     * @returns {HTMLElement[]}\n     */\n    _getDirectButtonSiblings(el) {\n        return [\"previous\", \"next\"].reduce((buttonSiblingsEls, side) => {\n            let siblingNode = el[`${side}Sibling`];\n            while (siblingNode) {\n                // If the node is an empty text node, or if it is a <br> tag or\n                // an invisible element, it is not taken into account.\n                if ((siblingNode.nodeType === 3 && !!siblingNode.textContent.match(/^\\s*$/)) ||\n                        (siblingNode.nodeType === 1 &&\n                        (siblingNode.nodeName === \"BR\" || !isVisible(siblingNode)))) {\n                    siblingNode = siblingNode[`${side}Sibling`];\n                    continue;\n                }\n                if (siblingNode.nodeType === 1 && siblingNode.classList.contains(\"btn\")) {\n                    buttonSiblingsEls.push(siblingNode);\n                }\n                break;\n            }\n            return buttonSiblingsEls;\n        }, []);\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAnyChange(e) {\n        if (!e.target.closest('input[type=\"text\"]')) {\n            this._adaptPreview();\n        }\n    }\n    /**\n     * @todo Adapt in master: in stable _onURLInput was both used as an event\n     * handler responding to url input events + a private method called at the\n     * widget lifecycle start. Originally both points were to update the link\n     * tools/dialog UI. It was later wanted to actually update the DOM... but\n     * should only be done in event handler part.\n     *\n     * This allows to differentiate the event handler part. In master, we should\n     * take the opportunity to also update the `_updatePreview` concept which\n     * updates the \"preview\" of the original link dialog but actually updates\n     * the real DOM for the \"new\" link tools.\n     *\n     * @private\n     */\n    __onURLInput() {\n        const inputValue = this.$el[0].querySelector('#o_link_dialog_url_input').value;\n        this.state.url = this._deduceUrl(inputValue, this.linkEl) || inputValue;\n        this._onURLInput(...arguments);\n    }\n    /**\n     * @private\n     */\n    _onURLInput() {\n        var $linkUrlInput = this.$el.find('#o_link_dialog_url_input');\n        let value = $linkUrlInput.val();\n        let isLink = !EMAIL_REGEX.test(value) && !PHONE_REGEX.test(value);\n        this._getIsNewWindowFormRow().toggleClass('d-none', !isLink);\n        this.$el.find('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);\n    }\n    /**\n     * @private\n     */\n    _onURLInputChange() {\n        this._adaptPreview();\n        // Make sure that if an entered URL is for an attachment, its related\n        // fields visibility ultimately gets applied.\n        this._determineAttachmentType(this.state.url).then(() => {\n            this.__onURLInput();\n        });\n    }\n}\n\n/**\n * Return the link element to edit. Create one from selection if none was\n * present in selection.\n *\n * @param {Node} [options.containerNode]\n * @param {Node} [options.startNode]\n * @returns {Object}\n */\nexport function getOrCreateLink({ containerNode, startNode } = {}) {\n    if (startNode) {\n        if ($(startNode).is('a')) {\n            return { link: startNode, needLabel: false };\n        } else {\n            $(startNode).wrap('<a href=\"#\"/>');\n            return { link: startNode.parentElement, needLabel: false };\n        }\n    }\n\n    const doc = containerNode && containerNode.ownerDocument || document;\n    let needLabel = false;\n    let link = getInSelection(doc, 'a');\n    const $link = $(link);\n    const range = getDeepRange(containerNode, {splitText: true, select: true, correctTripleClick: true});\n    if (!range) {\n        return {};\n    }\n    const isContained = containerNode.contains(range.startContainer) && containerNode.contains(range.endContainer);\n    if (link && (!$link.has(range.startContainer).length || !$link.has(range.endContainer).length)) {\n        // Expand the current link to include the whole selection.\n        let before = link.previousSibling;\n        while (before !== null && range.intersectsNode(before)) {\n            link.insertBefore(before, link.firstChild);\n            before = link.previousSibling;\n        }\n        let after = link.nextSibling;\n        while (after !== null && range.intersectsNode(after)) {\n            link.appendChild(after);\n            after = link.nextSibling;\n        }\n    } else if (!link && isContained) {\n        link = document.createElement('a');\n        if (range.collapsed) {\n            range.insertNode(link);\n            needLabel = true;\n        } else {\n            link.appendChild(range.extractContents());\n            range.insertNode(link);\n        }\n    }\n    return { link, needLabel };\n};\n", "/** @odoo-module **/\n\nimport { onMounted, useRef } from \"@odoo/owl\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { Link } from \"./link\";\n\nexport class LinkDialog extends Link {\n    static components = { Dialog };\n    static template = 'web_editor.LinkDialog';\n    static props = {\n        ...Link.props,\n        focusField: { type: String, optional: true },\n        close: { type: Function },\n        onClose: { type: Function },\n        onSave: { type: Function },\n    };\n    inputTextRef = useRef('inputText');\n    inputUrlRef = useRef('inputUrl');\n\n    setup() {\n        super.setup();\n        onMounted(() => {\n            this.$el.find('[name=\"link_style_color\"]').on('change', this._onTypeChange.bind(this));\n            this.$el.find('input[name=\"label\"]').on('input', this._adaptPreview.bind(this));\n            const el = this.props.focusField === 'url' ? this.inputUrlRef.el : this.inputTextRef.el;\n            el.focus();\n        });\n        this.env.dialogData.close = () => this.onDiscard();\n    }\n\n    /**\n     * @override\n     */\n    start() {\n        super.start();\n        this.buttonOptsCollapseEl = this.linkComponentWrapperRef.el.querySelector('#o_link_dialog_button_opts_collapse');\n        this.$styleInputs = this.$el.find('input.link-style');\n        this.$styleInputs.prop('checked', false).filter('[value=\"\"]').prop('checked', true);\n        if (this.initialNewWindow) {\n            this.$el.find('we-button.o_we_checkbox_wrapper').toggleClass('active', true);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    onSave(ev) {\n        ev.preventDefault();\n        var data = this._getData();\n        if (data === null) {\n            var $url = this.$el.find('input[name=\"url\"]');\n            $url.closest('.o_url_input').addClass('o_has_error').find('.form-control, .form-select').addClass('is-invalid');\n            $url.focus();\n            return;\n        }\n        var allWhitespace = /\\s+/gi;\n        var allStartAndEndSpace = /^\\s+|\\s+$/gi;\n        var allBtnTypes = /(^|[ ])(btn-secondary|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi;\n        data.classes = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');\n        if (data.classes.replace(allBtnTypes, ' ')) {\n            data.style = {\n                'background-color': '',\n                'color': '',\n            };\n        }\n        data.linkDialog = this;\n        this.props.close();\n        this.props.onSave(data);\n    }\n\n    onDiscard() {\n        this.props.onClose();\n        this.props.close();\n    }\n\n    onUrlKeydown(ev) {\n        const isAutoCompleteDropdownOpen = document.querySelector(\".o-autocomplete--dropdown-menu\");\n        if (ev.key === \"Enter\" && !isAutoCompleteDropdownOpen) {\n            this.onSave(ev);\n        }\n    }\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _adaptPreview() {\n        var data = this._getData();\n        if (data === null) {\n            return;\n        }\n        const attrs = {\n            target: '_blank',\n            href: data.url && data.url.length ? data.url : '#',\n            class: `${data.classes.replace(/float-\\w+/, '')} o_btn_preview`,\n        };\n\n        const $linkPreview = this.$el.find(\"#link-preview\");\n        $linkPreview.attr(attrs);\n        this._updateLinkContent($linkPreview, data, { force: true });\n    }\n    /**\n     * @override\n     */\n    _doStripDomain() {\n        return this.$el.find('#o_link_dialog_url_strip_domain').prop('checked');\n    }\n    /**\n     * @override\n     */\n    _getIsNewWindowFormRow() {\n        return this.$el.find('input[name=\"is_new_window\"]').closest('.row');\n    }\n    /**\n     * @override\n     */\n    _getLinkOptions() {\n        const options = [\n            'select[name=\"link_style_color\"] > option',\n            'select[name=\"link_style_size\"] > option',\n            'select[name=\"link_style_shape\"] > option',\n        ];\n        return this.$el.find(options.join(','));\n    }\n    /**\n     * @override\n     */\n    _getLinkShape() {\n        return this.$el.find('select[name=\"link_style_shape\"]').val() || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkSize() {\n        return this.$el.find('select[name=\"link_style_size\"]').val() || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkType() {\n        return this.$el.find('select[name=\"link_style_color\"]').val() || '';\n    }\n    /**\n     * @override\n     */\n    _isNewWindow(url) {\n        if (this.props.forceNewWindow) {\n            return this._isFromAnotherHostName(url);\n        } else {\n            return this.$el.find('input[name=\"is_new_window\"]').prop('checked');\n        }\n    }\n    /**\n     * @override\n     */\n    _setSelectOption($option, active) {\n        if ($option.is(\"input\")) {\n            $option.prop(\"checked\", active);\n        } else if (active) {\n            $option.parent().find('option').removeAttr('selected').removeProp('selected');\n            $option.parent().val($option.val());\n            $option.attr('selected', 'selected').prop('selected', 'selected');\n        }\n    }\n    /**\n     * @override\n     */\n    _updateOptionsUI() {\n        const el = this.linkComponentWrapperRef.el.querySelector('[name=\"link_style_color\"] option:checked');\n        $(this.buttonOptsCollapseEl).collapse(el && el.value ? 'show' : 'hide');\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onTypeChange() {\n        this._updateOptionsUI();\n    }\n    /**\n     * @override\n     */\n    _onURLInput() {\n        super._onURLInput(...arguments);\n        this.$el.find('#o_link_dialog_url_input').closest('.o_url_input').removeClass('o_has_error').find('.form-control, .form-select').removeClass('is-invalid');\n        this._adaptPreview();\n    }\n}\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ancestors } from '@web_editor/js/common/wysiwyg_utils';\nimport { KeepLast } from '@web/core/utils/concurrency';\nimport { browser } from \"@web/core/browser/browser\";\n\nexport class LinkPopoverWidget {\n    static createFor(params) {\n        const noLinkPopoverClass = \".o_no_link_popover, .carousel-control-prev, .carousel-control-next, .dropdown-toggle\";\n        // Target might already have a popover, eg cart icon in navbar\n        const alreadyPopover = $(params.target).data('bs.popover');\n        if (alreadyPopover || $(params.target).is(noLinkPopoverClass) || !!$(params.target).parents(noLinkPopoverClass).length) {\n            return null;\n        }\n        const popoverWidget = new this(params);\n        params.wysiwyg?.odooEditor.observerUnactive('LinkPopoverWidget');\n        popoverWidget.start(); // This is not async\n        params.wysiwyg?.odooEditor.observerActive('LinkPopoverWidget');\n        return popoverWidget;\n    };\n\n    template = `\n        <div class=\"d-flex\">\n            <span class=\"me-2 o_we_preview_favicon\"><i class=\"fa fa-globe\"></i><img class=\"align-baseline d-none\"></img></span>\n            <div class=\"w-100\">\n                <div class=\"d-flex\">\n                    <a href=\"#\" target=\"_blank\" class=\"o_we_url_link fw-bold flex-grow-1 text-truncate\" title=\"${_t('Open in a new tab')}\"></a>\n                    <a href=\"#\" class=\"mx-1 o_we_copy_link text-dark\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"${_t('Copy Link')}\">\n                        <i class=\"fa fa-clone\"></i>\n                    </a>\n                    <a href=\"#\" class=\"mx-1 o_we_edit_link text-dark\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"${_t('Edit Link')}\">\n                        <i class=\"fa fa-edit\"></i>\n                    </a>\n                    <a href=\"#\" class=\"ms-1 o_we_remove_link text-dark\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" title=\"${_t('Remove Link')}\">\n                        <i class=\"fa fa-chain-broken\"></i>\n                    </a>\n                </div>\n                <a href=\"#\" target=\"_blank\" class=\"o_we_full_url mt-1 text-muted d-none\" title=\"${_t('Open in a new tab')}\"></a>\n            </div>\n        </div>\n    `;\n\n    constructor(params) {\n        const template = document.createElement('template');\n        template.innerHTML = this.template;\n        this.el = template.content.firstElementChild;\n        this.$el = $(this.el);\n\n        this.wysiwyg = params.wysiwyg;\n        this.target = params.target;\n        this.notify = params.notify;\n        this.$target = $(params.target);\n        this.container = params.container || this.target.ownerDocument.body;\n        this.href = this.$target.attr('href'); // for template\n        this._keepLastPromise = new KeepLast();\n    }\n\n    /**\n     *\n     * @override\n     */\n    start() {\n        this.$urlLink = this.$el.find('.o_we_url_link');\n        this.$previewFaviconImg = this.$el.find('.o_we_preview_favicon img');\n        this.$previewFaviconFa = this.$el.find('.o_we_preview_favicon .fa');\n        this.$copyLink = this.$el.find('.o_we_copy_link');\n        this.$fullUrl = this.$el.find('.o_we_full_url');\n\n        this.$urlLink.attr('href', this.href);\n        this.$fullUrl.attr('href', this.href);\n        this.$el.find(`.o_we_edit_link`).on('click', this._onEditLinkClick.bind(this));\n        this.$el.find(`.o_we_remove_link`).on('click', this._onRemoveLinkClick.bind(this));\n\n        this.$copyLink.on(\"click\", this._onCopyLinkClick.bind(this));\n\n        // init tooltips & popovers\n        this.$el.find('[data-bs-toggle=\"tooltip\"]').tooltip({\n            delay: 0,\n            placement: 'bottom',\n            container: this.container,\n        });\n        const tooltips = [];\n        for (const el of this.$el.find('[data-bs-toggle=\"tooltip\"]').toArray()) {\n            tooltips.push(Tooltip.getOrCreateInstance(el));\n        }\n        let popoverShown = true;\n        this.$target.popover({\n            html: true,\n            content: this.$el,\n            placement: 'bottom',\n            // We need the popover to:\n            // 1. Open when the link is clicked or double clicked\n            // 2. Remain open when the link is clicked again (which `trigger: 'click'` is not doing)\n            // 3. Remain open when the popover content is clicked..\n            // 4. ..except if it the click was on a button of the popover content\n            // 5. Close when the user click somewhere on the page (not being the link or the popover content)\n            trigger: 'manual',\n            boundary: 'viewport',\n            container: this.container,\n        })\n        .on('show.bs.popover.link_popover', () => {\n            this._loadAsyncLinkPreview();\n            popoverShown = true;\n        })\n        .on('hide.bs.popover.link_popover', () => {\n            popoverShown = false;\n        })\n        .on('hidden.bs.popover.link_popover', () => {\n            for (const tooltip of tooltips) {\n                tooltip.hide();\n            }\n        })\n        .on('inserted.bs.popover.link_popover', () => {\n            const popover = Popover.getInstance(this.target);\n            popover.tip.classList.add('o_edit_menu_popover');\n        })\n        .popover('show');\n\n        this.popover = Popover.getInstance(this.target);\n        this.$target.on('mousedown.link_popover', (e) => {\n            if (!popoverShown) {\n                this.$target.popover('show');\n            }\n        });\n        this.$target.on('href_changed.link_popover', (e) => {\n            // Do not change shown/hidden state.\n            if (popoverShown) {\n                this._loadAsyncLinkPreview();\n            }\n        });\n        const onClickDocument = (e) => {\n            if (popoverShown) {\n                const hierarchy = [e.target, ...ancestors(e.target)];\n                if (\n                    !(\n                        hierarchy.includes(this.$target[0]) ||\n                        (hierarchy.includes(this.$el[0]) &&\n                            !hierarchy.some(x => x.tagName && x.tagName === 'A' && (x === this.$urlLink[0] || x === this.$fullUrl[0])))\n                    )\n                ) {\n                    // Note: For buttons of the popover, their listeners should\n                    // handle the hide themselves to avoid race conditions.\n                    this.popover.hide();\n                }\n            }\n        };\n        $(document).on('mouseup.link_popover', onClickDocument);\n        if (document !== this.wysiwyg.odooEditor.document) {\n            $(this.wysiwyg.odooEditor.document).on('mouseup.link_popover', onClickDocument);\n        }\n\n        // Update popover's content and position upon changes\n        // on the link's label or href.\n        this._observer = new MutationObserver(records => {\n            if (!popoverShown) {\n                return;\n            }\n            if (records.some(record => record.type === 'attributes')) {\n                this._loadAsyncLinkPreview();\n            }\n            this.$target.popover('update');\n        });\n        this._observer.observe(this.target, {\n            subtree: true,\n            characterData: true,\n            attributes: true,\n            attributeFilter: ['href'],\n        });\n    }\n    /**\n     *\n     * @override\n     */\n    destroy() {\n        // FIXME those are never destroyed, so this could be a cause of memory\n        // leak. However, it is only one leak per click on a link during edit\n        // mode so this should not be a huge problem.\n        this.$target.off('.link_popover');\n        $(document).off('.link_popover');\n        $(this.wysiwyg.odooEditor.document).off('.link_popover');\n        this.$target.popover('dispose');\n        this._observer.disconnect();\n    }\n\n    /**\n     *  Hide the popover.\n     */\n    hide() {\n        this.$target.popover('hide');\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Fetches and gets the link preview data (title, description..).\n     * For external URL, only the favicon will be loaded.\n     *\n     * @private\n     */\n    async _loadAsyncLinkPreview() {\n        let url;\n        if (this.target.href === '') {\n            this._resetPreview('');\n            this.$previewFaviconFa.removeClass('fa-globe').addClass('fa-question-circle-o');\n            return;\n        }\n        try {\n            url = new URL(this.target.href); // 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.notify(_t(\"This URL is invalid. Preview couldn't be updated.\"), {\n                type: 'danger',\n            });\n            return;\n        }\n\n        this._resetPreview(url);\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.$previewFaviconFa.toggleClass(`fa-globe ${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.$previewFaviconImg.attr({\n                'src': `https://www.google.com/s2/favicons?sz=16&domain=${encodeURIComponent(url)}`\n            }).removeClass('d-none');\n            this.$previewFaviconFa.addClass('d-none');\n        } else {\n            await this._keepLastPromise.add($.get(this.target.href)).then(content => {\n                const parser = new window.DOMParser();\n                const doc = parser.parseFromString(content, \"text/html\");\n\n                // Get\n                const favicon = doc.querySelector(\"link[rel~='icon']\");\n                const ogTitle = doc.querySelector(\"[property='og:title']\");\n                const title = doc.querySelector(\"title\");\n\n                // Set\n                if (favicon) {\n                    this.$previewFaviconImg.attr({'src': favicon.href}).removeClass('d-none');\n                    this.$previewFaviconFa.addClass('d-none');\n                }\n                if (ogTitle || title) {\n                    this.$urlLink.text(ogTitle ? ogTitle.getAttribute('content') : title.text.trim());\n                }\n                this.$fullUrl.removeClass('d-none').addClass('o_we_webkit_box');\n            }).catch(error => {\n                // HTML 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            }).finally(() => {\n                this.$target.popover('update');\n            });\n        }\n    }\n    /**\n     * Resets the preview elements visibility. Particularly useful when changing\n     * the link url from an internal to an external one and vice versa.\n     *\n     * @private\n     * @param {string} url\n     */\n    _resetPreview(url) {\n        this.$previewFaviconImg.addClass('d-none');\n        this.$previewFaviconFa.removeClass('d-none fa-question-circle-o fa-envelope-o fa-phone').addClass('fa-globe');\n        this.$urlLink.add(this.$fullUrl).text(url || _t('No URL specified')).attr('href', url || null);\n        this.$fullUrl.addClass('d-none').removeClass('o_we_webkit_box');\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Opens the Link Dialog.\n     *\n     * TODO The editor instance should be reached a proper way\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onEditLinkClick(ev) {\n        ev.preventDefault();\n        this.wysiwyg.toggleLinkTools({\n            forceOpen: true,\n            link: this.$target[0],\n        });\n        ev.stopImmediatePropagation();\n        this.popover.hide();\n    }\n    /**\n     * Removes the link/anchor.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onRemoveLinkClick(ev) {\n        ev.preventDefault();\n        this.wysiwyg.removeLink();\n        ev.stopImmediatePropagation();\n        this.popover.hide();\n    }\n    /**\n     * Copy the link/anchor\n     * \n     * @private\n     * @param {Event} ev\n     */\n    async _onCopyLinkClick(ev) {\n        ev.preventDefault();\n        await browser.navigator.clipboard.writeText(this.target.href);\n        this.$copyLink.tooltip('hide');\n        this.notify(_t(\"Link copied to clipboard.\"), {\n            type: 'success',\n        });\n        this.popover.hide();\n    }\n}\n", "/** @odoo-module **/\n\nimport { Link } from \"./link\";\nimport { ColorPalette } from '@web_editor/js/wysiwyg/widgets/color_palette';\nimport weUtils from \"@web_editor/js/common/utils\";\nimport {\n    onMounted,\n    onWillUnmount,\n    onWillDestroy,\n    useState,\n} from \"@odoo/owl\";\nimport { normalizeCSSColor } from '@web/core/utils/colors';\n\n/**\n * Allows to customize link content and style.\n */\nexport class LinkTools extends Link {\n    static template = 'web_editor.LinkTools';\n    static props = {\n        ...Link.props,\n        wysiwyg: { type: Object },\n        $button: { type: Object },\n        onColorCombinationClassChange: { type: Function, optional: true },\n        onPreApplyLink: { type: Function, optional: true },\n        onPostApplyLink: { type: Function, optional: true },\n        onDestroy: { type: Function, optional: true },\n        getColorpickerTemplate: { type: Function, optional: true },\n    };\n    static defaultProps = {\n        ...Link.defaultProps,\n        onColorCombinationClassChange: () => {},\n        onPreApplyLink: () => {},\n        onPostApplyLink: () => {},\n        onDestroy: () => {},\n    };\n    static components = { ColorPalette };\n    colorpickerProps = useState({\n        'color': { selectedColor: undefined },\n        'background-color': { selectedColor: undefined },\n        'border-color': { selectedColor: undefined },\n    });\n    colorpickers = {\n        'color': { colorNames: null },\n        'background-color': { colorNames: null },\n        'border-color': { colorNames: null },\n    };\n    state = useState({\n        showLinkSizeRow: true,\n        showLinkCustomColor: true,\n        showLinkShapeRow: true,\n        isDocument: false,\n        directDownload: true,\n    });\n\n    setup() {\n        super.setup(...arguments);\n        onMounted(() => {\n            this._observer = new MutationObserver(records => {\n                if (records.some(record => record.type === 'attributes')) {\n                    this.state.url = this.props.link.getAttribute('href') || '';\n                    this._setUrl();\n                }\n                this._updateLabelInput();\n            });\n            this._observerOptions = {\n                subtree: true,\n                childList: true,\n                characterData: true,\n                attributes: true,\n                attributeFilter: ['href'],\n            };\n            this._observer.observe(this.props.link, this._observerOptions);\n        });\n        onWillUnmount(() => {\n            this._observer.disconnect();\n        });\n        onWillDestroy(() => {\n            const $contents = this.$link.contents();\n            if (shouldUnlink(this.$link[0], this.colorCombinationClass)) {\n                $contents.unwrap();\n            }\n            this.props.onDestroy();\n        });\n    }\n    /**\n     * @override\n     */\n    async willUpdateProps(newProps) {\n        await super.willUpdateProps(newProps);\n        this.$link = newProps.link ? $(newProps.link) : this.link;\n        this._setSelectOptionFromLink();\n        this._updateOptionsUI();\n        this._updateLabelInput();\n        this._checkDocumentState();\n    }\n\n    /**\n     * @override\n     */\n    async _updateState() {\n        await super._updateState(...arguments);\n        // Keep track of each selected custom color and colorpicker.\n        this.customColors = {};\n        this.PREFIXES = {\n            'color': 'text-',\n            'background-color': 'bg-',\n        };\n        this._updateInitialNewWindowUI();\n    }\n    _updateInitialNewWindowUI() {\n        // TODO In master, put initialNewWindow in state.\n        // Adjust rendered initialNewWindow because changes are ignored by Owl.\n        if (this.$el && this.$el[0]) {\n            const checkboxEl = this.$el[0].querySelector(\"we-checkbox[name='is_new_window'\");\n            if (checkboxEl) {\n                checkboxEl.checked = this.initialNewWindow ? \"checked\" : \"\";\n                const buttonEl = checkboxEl.closest(\"we-button\");\n                if (buttonEl) {\n                    buttonEl.classList[this.initialNewWindow ? \"add\" : \"remove\"](\"active\");\n                }\n            }\n        }\n    }\n    /**\n     * @override\n     */\n    async start() {\n        const ret = await super.start(...arguments);\n        this.$el.on('click', 'we-select we-button', this._onPickSelectOption.bind(this));\n        this.$el.on('click', 'we-checkbox', this._onClickCheckbox.bind(this));\n        this.$el.on('change', '.link-custom-color-border input', this._onChangeCustomBorderWidth.bind(this));\n        this.$el.on('keypress', '.link-custom-color-border input', this._onKeyPressCustomBorderWidth.bind(this));\n        this.$el.on('click', 'we-select [name=\"link_border_style\"] we-button', this._onBorderStyleSelectOption.bind(this));\n        this.$el.on('input', 'input[name=\"label\"]', this._onLabelInput.bind(this));\n\n        this._setSelectOptionFromLink();\n        this._updateOptionsUI();\n\n        if (!this.linkEl.href && this.state.url) {\n            // Link URL was deduced from label. Apply changes to DOM.\n            this.__onURLInput();\n        }\n        this._checkDocumentState();\n\n        return ret;\n    }\n    applyLinkToDom() {\n        this._observer.disconnect();\n        this.props.onPreApplyLink();\n        super.applyLinkToDom(...arguments);\n        this.props.wysiwyg.odooEditor.historyStep();\n        this.props.onPostApplyLink();\n        this._observer.observe(this.props.link, this._observerOptions);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    focusUrl() {\n        this.$el[0].scrollIntoView();\n        super.focusUrl(...arguments);\n    }\n\n    openDocumentDialog() {\n        this.props.wysiwyg.openMediaDialog({\n            resModel: \"ir.ui.view\",\n            useMediaLibrary: true,\n            noImages: true,\n            noIcons: true,\n            noVideos: true,\n            save: async (link) => {\n                this.initialNewWindow = this.initialIsNewWindowFromProps;\n                this._updateInitialNewWindowUI();\n                let relativeUrl = link.href.substr(window.location.origin.length);\n                await this._determineAttachmentType(relativeUrl.split(\"?\")[0]);\n                if (this.isLastAttachmentUrl) {\n                    relativeUrl = relativeUrl.replace(\"&download=true\", \"\");\n                }\n                this.$el[0].querySelector(\"#o_link_dialog_url_input\").value = relativeUrl;\n                this.__onURLInput();\n            },\n        });\n    }\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n    _setSelectOptionFromLink() {\n        super._setSelectOptionFromLink(...arguments);\n        const link = this.$link[0];\n        const customStyleProps = ['color', 'background-color', 'background-image', 'border-width', 'border-style', 'border-color'];\n        const shapeClasses = ['btn-outline-primary', 'btn-outline-secondary', 'btn-fill-primary', 'btn-fill-secondary', 'rounded-circle', 'flat'];\n        if (customStyleProps.some(s => link.style[s]) || shapeClasses.some(c => link.classList.contains(c))) {\n            // Force custom style if style or shape exists on the link.\n            const customOption = this.$el[0].querySelector('[name=\"link_style_color\"] we-button[data-value=\"custom\"]');\n            this._setSelectOption($(customOption), true);\n        }\n    }\n    /**\n     * @override\n     */\n    _adaptPreview() {\n        var data = this._getData();\n        if (data === null) {\n            return;\n        }\n        this.applyLinkToDom(data);\n    }\n    /**\n     * @override\n     */\n    _doStripDomain() {\n        return this.$el.find('we-checkbox[name=\"do_strip_domain\"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');\n    }\n    /**\n     * @override\n     */\n    _getIsNewWindowFormRow() {\n        return this.$el.find('we-checkbox[name=\"is_new_window\"]').closest('we-row');\n    }\n    /**\n     * @override\n     */\n    _getLinkOptions() {\n        const options = [\n            'we-selection-items[name=\"link_style_color\"] > we-button',\n            'we-selection-items[name=\"link_style_size\"] > we-button',\n            'we-selection-items[name=\"link_style_shape\"] > we-button',\n        ];\n        return this.$el.find(options.join(','));\n    }\n    /**\n     * @override\n     */\n    _getLinkShape() {\n        return this.$el.find('we-selection-items[name=\"link_style_shape\"] we-button.active').data('value') || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkSize() {\n        return this.$el.find('we-selection-items[name=\"link_style_size\"] we-button.active').data('value') || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkType() {\n        return this.$el.find('we-selection-items[name=\"link_style_color\"] we-button.active').data('value') || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomTextColor() {\n        return this.customColors['color'];\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomBorder() {\n        return this.customColors['border-color'];\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomBorderWidth() {\n        return this.$el.find('.link-custom-color-border input').val() || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomBorderStyle() {\n        return this.$el.find('.link-custom-color-border we-button.active').data('value') || '';\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomFill() {\n        return this.customColors['background-color'];\n    }\n    /**\n     * @override\n     */\n    _getLinkCustomClasses() {\n        let textClass = this.customColors['color'];\n        const colorPickerFg = this.colorpickers['color'].colorNames;\n        if (\n            !textClass ||\n            !colorPickerFg ||\n            !weUtils.computeColorClasses(colorPickerFg, 'text-').includes(textClass)\n        ) {\n            textClass = '';\n        }\n        let fillClass = this.customColors['background-color'];\n        const colorPickerBg = this.colorpickers['background-color'].colorNames;\n        if (\n            !fillClass ||\n            !colorPickerBg ||\n            !weUtils.computeColorClasses(colorPickerBg, 'bg-').includes(fillClass)\n        ) {\n            fillClass = '';\n        }\n        return ` ${textClass} ${fillClass}`;\n    }\n    /**\n     * @override\n     */\n    _isNewWindow(url) {\n        if (this.props.forceNewWindow) {\n            return this._isFromAnotherHostName(url);\n        } else {\n            return this.$el.find('we-checkbox[name=\"is_new_window\"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');\n        }\n    }\n    /**\n     * @override\n     */\n    _setSelectOption($option, active) {\n        $option.toggleClass('active', active);\n        if (active) {\n            $option.closest('we-select').find('we-toggler').text($option.text());\n            // ensure only one option is active in the dropdown\n            $option.siblings('we-button').removeClass(\"active\");\n        }\n    }\n    /**\n     * @override\n     */\n    _updateOptionsUI() {\n        const el = this.$el[0].querySelector('[name=\"link_style_color\"] we-button.active');\n        if (el) {\n            this.colorCombinationClass = el.dataset.value;\n            // Hide the size option if the link is an unstyled anchor.\n            this.state.showLinkSizeRow = Boolean(this.colorCombinationClass);\n\n            // // Show custom colors and shape only for Custom style.\n            this.state.showLinkCustomColor = el.dataset.value === 'custom';\n            this.state.showLinkShapeRow = el.dataset.value === 'custom';\n\n            this.props.onColorCombinationClassChange(this.colorCombinationClass);\n\n            this._updateColorpicker('color');\n            this._updateColorpicker('background-color');\n            this._updateColorpicker('border-color');\n\n            const borderWidth = this.linkEl.style['border-width'];\n            const numberAndUnit = weUtils.getNumericAndUnit(borderWidth);\n            this.$el.find('.link-custom-color-border input').val(numberAndUnit ? numberAndUnit[0] : \"1\");\n            let borderStyle = this.linkEl.style['border-style'];\n            if (!borderStyle || borderStyle === 'none') {\n                borderStyle = 'solid';\n            }\n            const $activeBorderStyleButton = this.$el.find(`.link-custom-color-border [name=\"link_border_style\"] we-button[data-value=\"${borderStyle}\"]`);\n            $activeBorderStyleButton.addClass('active');\n            $activeBorderStyleButton.siblings('we-button').removeClass(\"active\");\n            const $activeBorderStyleToggler = $activeBorderStyleButton.closest('we-select').find('we-toggler');\n            $activeBorderStyleToggler.empty();\n            $activeBorderStyleButton.find('div').clone().appendTo($activeBorderStyleToggler);\n        }\n    }\n    /**\n     * Updates the colorpicker associated to a given property - updated with its selected color.\n     *\n     * @private\n     * @param {string} cssProperty\n     */\n    _updateColorpicker(cssProperty) {\n        const prefix = this.PREFIXES[cssProperty];\n\n        // Update selected color.\n        const colorNames = this.colorpickers[cssProperty].colorNames;\n        let color = this.linkEl.style[cssProperty];\n        const colorClasses = prefix ? weUtils.computeColorClasses(colorNames, prefix) : [];\n        const colorClass = prefix && weUtils.getColorClass(this.linkEl, colorNames, prefix);\n        const isColorClass = colorClasses.includes(colorClass);\n        if (isColorClass) {\n            color = colorClass;\n        } else if (cssProperty === 'background-color') {\n            const gradientColor = this.linkEl.style['background-image'];\n            if (weUtils.isColorGradient(gradientColor)) {\n                color = gradientColor;\n            }\n        }\n        this.customColors[cssProperty] = color;\n        if (cssProperty === 'border-color') {\n            // Highlight matching named color if any.\n            const colorName = colorNames[normalizeCSSColor(color)];\n            this.colorpickerProps[cssProperty].selectedColor = colorName || color;\n        } else {\n            this.colorpickerProps[cssProperty].selectedColor = isColorClass ? color.replace(prefix, '') : color;\n        }\n\n        // Update preview.\n        const $colorPreview = this.$el.find('.link-custom-color-' + (cssProperty === 'border-color' ? 'border' : cssProperty === 'color' ? 'text' : 'fill') + ' .o_we_color_preview');\n        const previewClasses = weUtils.computeColorClasses(colorNames, 'bg-');\n        $colorPreview[0].classList.remove(...previewClasses);\n        if (isColorClass) {\n            $colorPreview.css('background-color', `var(--we-cp-${color.replace(prefix, '')}`);\n            $colorPreview.css('background-image', '');\n        } else {\n            $colorPreview.css('background-color', weUtils.isColorGradient(color) ? 'rgba(0, 0, 0, 0)' : color);\n            $colorPreview.css('background-image', weUtils.isColorGradient(color) ? color : '');\n        }\n    }\n\n    /**\n     * @private\n     */\n    _onColorpaletteSetColorNames(cssProperty, colorNames) {\n        this.colorpickers[cssProperty].colorNames = colorNames;\n    }\n    /**\n     * @private\n     */\n    _onColorpaletteColorPicked(cssProperty, params) {\n        // Reset color styles in link content to make sure new color is not hidden.\n        // Only done when applied to avoid losing state during preview.\n        const selection = window.getSelection();\n        const range = document.createRange();\n        range.selectNodeContents(this.linkEl);\n        selection.removeAllRanges();\n        selection.addRange(range);\n        this.props.wysiwyg.odooEditor.execCommand('applyColor', '', 'color');\n        this.props.wysiwyg.odooEditor.execCommand('applyColor', '', 'backgroundColor');\n\n        this._colorpaletteApply(cssProperty, params);\n\n        this.props.wysiwyg.odooEditor.historyStep();\n        this._updateOptionsUI();\n    }\n    /**\n     * @private\n     */\n    _colorpaletteApply(cssProperty, params) {\n        const prefix = this.PREFIXES[cssProperty];\n        let color = params.color;\n        const colorNames = this.colorpickers[cssProperty].colorNames;\n        const colorClasses = prefix ? weUtils.computeColorClasses(colorNames, prefix) : [];\n        const colorClass = `${prefix}${color}`;\n        if (colorClasses.includes(colorClass)) {\n            color = colorClass;\n        } else if (colorNames.includes(color)) {\n            // Store as color value.\n            color = weUtils.getCSSVariableValue(color);\n        }\n        this.customColors[cssProperty] = color;\n        this.applyLinkToDom(this._getData());\n    }\n    /**\n     * Updates the label input with the DOM content of the link.\n     *\n     * @private\n     */\n    _updateLabelInput() {\n        if (this.$el) {\n            this.$el[0].querySelector('#o_link_dialog_label_input').value =\n                weUtils.getLinkLabel(this.linkEl);\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onClickCheckbox(ev) {\n        const $target = $(ev.target);\n        $target.closest('we-button.o_we_checkbox_wrapper').toggleClass('active');\n        if (ev.target.getAttribute(\"name\") === \"direct_download\") {\n            const el = ev.target.closest(\"we-button.o_we_checkbox_wrapper\");\n            const urlInputEl = this.$el[0].querySelector(\"#o_link_dialog_url_input\");\n            let href = urlInputEl.value.replace(\"&download=true\", \"\");\n            if (el.classList.contains(\"active\")) {\n                href += \"&download=true\";\n                this.state.directDownload = true;\n            }\n            else {\n                this.state.directDownload = false;\n            }\n            urlInputEl.value = href;\n            this.state.url = href;\n        } \n        this._adaptPreview();\n    }\n    _onPickSelectOption(ev) {\n        const $target = $(ev.target);\n        if ($target.closest('[name=\"link_border_style\"]').length) {\n            return;\n        }\n        const $select = $target.closest('we-select');\n        $select.find('we-selection-items we-button').toggleClass('active', false);\n        this._setSelectOption($target, true);\n        this._updateOptionsUI();\n        this._adaptPreview();\n    }\n    /**\n     * Sets the border width on the link.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onChangeCustomBorderWidth(ev) {\n        const value = ev.target.value;\n        if (parseInt(value) >= 0) {\n            this.$link.css('border-width', value + 'px');\n        }\n    }\n    /**\n     * Sets the border width on the link when enter is pressed.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onKeyPressCustomBorderWidth(ev) {\n        if (ev.key === \"Enter\") {\n            this._onChangeCustomBorderWidth(ev);\n        }\n    }\n    /**\n     * Sets the border style on the link.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onBorderStyleSelectOption(ev) {\n        const value = ev.currentTarget.dataset.value;\n        if (value) {\n            this.$link.css('border-style', value);\n            const $target = $(ev.currentTarget);\n            const $activeBorderStyleToggler = $target.closest('we-select').find('we-toggler');\n            $activeBorderStyleToggler.empty();\n            $target.find('div').clone().appendTo($activeBorderStyleToggler);\n            // Ensure only one option is active in the dropdown.\n            $target.addClass('active');\n            $target.siblings('we-button').removeClass(\"active\");\n            this.props.wysiwyg.odooEditor.historyStep();\n        }\n    }\n    _checkDocumentState() {\n        this.state.isDocument = false;\n        this.state.directDownload = true;\n        const url = this.state.url;\n        if (url && url.startsWith(\"/web/content/\")) {\n            this.state.isDocument = !this.isLastAttachmentUrl;\n            this.state.directDownload = url.includes(\"&download=true\");\n        }\n    }\n    /**\n     * @override\n     */\n    __onURLInput() {\n        super.__onURLInput(...arguments);\n        this.props.wysiwyg.odooEditor.historyPauseSteps('_onURLInput');\n        this._checkDocumentState();\n        this._syncContent();\n        this._adaptPreview();\n        this.props.wysiwyg.odooEditor.historyUnpauseSteps('_onURLInput');\n    }\n    /**\n     * Updates the DOM content of the link with the input value.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onLabelInput(ev) {\n        const data = this._getData();\n        if (!data) {\n            return;\n        }\n        this._observer.disconnect();\n        // Force update of link's content with new data using 'force: true'.\n        // Without this, no update if input is same as original text.\n        this._updateLinkContent(this.$link, data, {force: true});\n        this._observer.observe(this.props.link, this._observerOptions);\n    }\n    /* If content is equal to previous URL, update it to match current URL.\n     *\n     * @private\n     */\n    _syncContent() {\n        const previousUrl = this.linkEl.getAttribute('href');\n        if (!previousUrl) {\n            return;\n        }\n        const protocolLessPrevUrl = previousUrl.replace(/^https?:\\/\\/|^mailto:/i, '');\n        const content = weUtils.getLinkLabel(this.linkEl);\n        if (\n            (content === previousUrl || content === protocolLessPrevUrl) &&\n            this.linkComponentWrapperRef.el\n        ) {\n            const newUrl = this.linkComponentWrapperRef.el.querySelector('input[name=\"url\"]').value;\n            const protocolLessNewUrl = newUrl.replace(/^https?:\\/\\/|^mailto:/i, '')\n            const newContent = content.replace(protocolLessPrevUrl, protocolLessNewUrl);\n            this.linkComponentWrapperRef.el.querySelector('#o_link_dialog_label_input').value = newContent;\n            this._onLabelInput();\n        }\n    }\n}\n\nexport function shouldUnlink(link, colorCombinationClass) {\n    return (!link.getAttribute(\"href\") && !link.matches(\".oe_unremovable\")) && !colorCombinationClass;\n}\n", "/** @odoo-module **/\n\nimport { utils as uiUtils } from \"@web/core/ui/ui_service\";\nimport { ColorPalette } from \"@web_editor/js/wysiwyg/widgets/color_palette\";\n\nimport {\n    Component,\n    onMounted,\n    onWillStart,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\n\nimport { useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\nimport { loadLanguages } from \"@web/core/l10n/translation\";\n\nexport class Toolbar extends Component {\n    static template = 'web_editor.toolbar';\n    static components = { ColorPalette };\n    static props = {\n        dropDirection: { type: String, optional: true },\n\n        showChecklist: { type: Boolean, optional: true },\n        showColors: { type: Boolean, optional: true },\n        showFontSize: { type: Boolean, optional: true },\n        useFontSizeInput: { type: Boolean, optional: true },\n        showHistory: { type: Boolean, optional: true },\n        showRemoveFormat: { type: Boolean, optional: true },\n\n        showStyle: { type: Boolean, optional: true },\n        showJustify: { type: Boolean, optional: true },\n        showList: { type: Boolean, optional: true },\n        showLink: { type: Boolean, optional: true },\n\n        showImageShape: { type: Boolean, optional: true },\n        showImagePadding: { type: Boolean, optional: true },\n        showImageWidth: { type: Boolean, optional: true },\n        showImageEdit: { type: Boolean, optional: true },\n\n        showHeading1: { type: Boolean, optional: true },\n        showHeading2: { type: Boolean, optional: true },\n        showHeading3: { type: Boolean, optional: true },\n        showHeading4: { type: Boolean, optional: true },\n        showHeading5: { type: Boolean, optional: true },\n        showHeading6: { type: Boolean, optional: true },\n\n        onColorpaletteDropdownShow: { type: Function, optional: true },\n        onColorpaletteDropdownHide: { type: Function, optional: true },\n        textColorPaletteProps: { type: Object },\n        backgroundColorPaletteProps: { type: Object },\n\n        slots: { type: Object, optional: true },\n    };\n    static defaultProps = {\n        dropDirection: 'dropdown',\n\n        showChecklist: true,\n        showColors: true,\n        showFontSize: true,\n        useFontSizeInput: false,\n        showHistory: false,\n        showRemoveFormat: true,\n\n        showStyle: true,\n        showJustify: true,\n        showList: true,\n        showLink: true,\n\n        showImageShape: true,\n        showImagePadding: true,\n        showImageWidth: true,\n        showImageEdit: true,\n\n        showHeading1: true,\n        showHeading2: true,\n        showHeading3: true,\n        showHeading4: true,\n        showHeading5: true,\n        showHeading6: true,\n\n        onColorpaletteDropdownShow: () => {},\n        onColorpaletteDropdownHide: () => {},\n    };\n\n    colorDropdownRef = {\n        text: useRef(\"textColorpickerDropdown\"),\n        background: useRef(\"backgroundColorpaletteDropdown\"),\n    }\n\n    setup() {\n        this.orm = useService(\"orm\");\n        this.state = useState({ languages : [] });\n        onMounted(() => {\n            for (const [colorType, ref] of Object.entries(this.colorDropdownRef)) {\n                const dropdown = ref.el;\n                if (!dropdown) continue;\n                // If the element is within an iframe, access the jquery loaded in\n                // the iframe because it is the one who will trigger the dropdown\n                // events (i.e hide.bs.dropdown and show.bs.dropdown).\n                const $ = dropdown.ownerDocument.defaultView.$;\n                const $dropdown = $(dropdown);\n                $dropdown.on('show.bs.dropdown', () => {\n                    this.props.onColorpaletteDropdownShow(colorType);\n                });\n                $dropdown.on('hide.bs.dropdown', (ev) => this.props.onColorpaletteDropdownHide(ev));\n            }\n        });\n        onWillStart(() => {\n            this.state.isPublicUser = !user.userId;\n\n            if (!this.state.isPublicUser) {\n                loadLanguages(this.orm).then((res) => {\n                    this.state.languages = res;\n                });\n            }\n        });\n    }\n\n    isSmall() {\n        return uiUtils.isSmall();\n    }\n}\n", "import { isBrowserFirefox } from \"@web/core/browser/feature_detection\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport {\n    useRef,\n    useState,\n    useEffect,\n    Component,\n    onMounted,\n} from \"@odoo/owl\";\nimport { localization } from \"@web/core/l10n/localization\";\n\nexport class RenameCustomSnippetDialog extends Component {\n    static template = \"web_editor.RenameCustomSnippetDialog\";\n    static props = {\n        close: Function,\n        currentName: String,\n        confirm: Function,\n    };\n\n    static components = {\n        Dialog,\n    };\n    setup() {\n        this.renameInputRef = useRef(\"renameInput\");\n    }\n    onClickConfirm() {\n        this.props.confirm(this.renameInputRef.el.value);\n        this.props.close();\n    }\n    onClickDiscard() {\n        this.props.close();\n    }\n}\nexport class AddSnippetDialog extends Component {\n    static template = \"web_editor.AddSnippetDialog\";\n    static props = {\n        close: Function,\n        snippets: Object,\n        groupSelected: String,\n        optionsSnippets: String,\n        frontendDirection: String,\n        installModule: Function,\n        addSnippet: Function,\n        deleteCustomSnippet: Function,\n        renameCustomSnippet: Function,\n    };\n\n    static components = {\n        Dialog,\n        RenameCustomSnippetDialog,\n    };\n\n    setup() {\n        this.iframeRef = useRef(\"iframe\");\n        this.snippetGroups = this.getSnippetGroups();\n\n        this.dialog = useService(\"dialog\");\n\n        this.state = useState({\n            groupSelected: [],\n            search: \"\",\n        });\n\n        onMounted(async () => {\n            const isFirefox = isBrowserFirefox();\n            if (isFirefox) {\n                // Make sure empty preview iframe is loaded.\n                // This event is never triggered on Chrome.\n                await new Promise(resolve => {\n                    this.iframeDocument.body.onload = resolve;\n                });\n            }\n            this.iframeDocument.documentElement.classList.add(\"o_add_snippets_preview\");\n            this.iframeDocument.body.style.setProperty(\"direction\", localization.direction);\n            await this.insertStyle().then(() => {\n                this.iframeRef.el.classList.add(\"show\");\n            });\n            this.state.groupSelected = this.props.groupSelected;\n        });\n\n        this.currentInsertSnippetsCallID = 0;\n        useEffect(\n            () => {\n                this.insertSnippets();\n            },\n            () => [this.state.groupSelected, this.state.search, [...this.props.snippets]]\n        );\n    }\n\n    get iframeDocument() {\n        return this.iframeRef.el.contentDocument;\n    }\n    /**\n     * Gets snippet groups.\n     *\n     * @returns {object} snippets\n     */\n    getSnippetGroups() {\n        return [...this.props.snippets.values()]\n            .filter(snippet =>\n                !snippet.excluded\n                && (snippet.category.id === \"snippet_groups\")\n                && snippet.snippetGroup\n            )\n            .map(snippet => ({\n                displayName: snippet.displayName,\n                name: snippet.snippetGroup,\n                selected: snippet.snippetGroup === this.props.groupSelected,\n            }));\n    }\n    /**\n     * Inserts the snippets from the selected snippetGroup into the <iframe>.\n     */\n    async insertSnippets() {\n        const insertSnippetsCallID = ++this.currentInsertSnippetsCallID;\n\n        // First, filter out snippets which are never supposed to be shown\n        // (excluded ones, inner content ones, ...).\n        let snippetsToDisplay = [...this.props.snippets.values()].filter(snippet => {\n            // Note: custom ones have \"custom\" group, but inner ones (custom or\n            // not) have no group.\n            return !snippet.excluded && snippet.group;\n        });\n\n        if (this.state.search) {\n            const search = this.state.search;\n            const selectorSearch = /^s_[\\w-]*$/.test(search) && `[class^=\"${search}\"], [class*=\" ${search}\"]`;\n            const lowerCasedSearch = search.toLowerCase();\n            const strMatches = str => str.toLowerCase().includes(lowerCasedSearch);\n            snippetsToDisplay = snippetsToDisplay.filter(snippet => {\n                return selectorSearch && (\n                        snippet.baseBody.matches(selectorSearch)\n                        || snippet.baseBody.querySelector(selectorSearch)\n                    )\n                    || strMatches(snippet.category.text)\n                    || strMatches(snippet.displayName)\n                    || strMatches(snippet.data.oeKeywords || '');\n            });\n            // Make sure to show the snippets that \"better\" match first\n            if (selectorSearch) {\n                snippetsToDisplay.sort((snippetA, snippetB) => {\n                    // If the search is exactly equal to a snippet xmlid, show\n                    // that snippet first.\n                    if (snippetA.data.snippet === search) {\n                        return -1;\n                    }\n                    if (snippetB.data.snippet === search) {\n                        return 1;\n                    }\n\n                    // If the search is a full class name used on the snippet\n                    // root node, show that snippet first.\n                    const aHasExactClassOnRoot = snippetA.baseBody.classList.contains(search);\n                    const bHasExactClassOnRoot = snippetB.baseBody.classList.contains(search);\n                    if (aHasExactClassOnRoot !== bHasExactClassOnRoot) {\n                        return aHasExactClassOnRoot ? -1 : 1;\n                    }\n\n                    // Otherwise show a partial class match of a snippet first\n                    // if it happens on the root node.\n                    const aHasPartialClassOnRoot = snippetA.baseBody.matches(selectorSearch);\n                    const bHasPartialClassOnRoot = snippetB.baseBody.matches(selectorSearch);\n                    if (aHasPartialClassOnRoot !== bHasPartialClassOnRoot) {\n                        return aHasPartialClassOnRoot ? -1 : 1;\n                    }\n\n                    return 0;\n                });\n            }\n        } else {\n            // No search: display the currently selected tab (group)\n            snippetsToDisplay = snippetsToDisplay.filter(snippet => {\n                return snippet.group === this.state.groupSelected;\n            });\n        }\n\n        // Create the new 2-column structure\n        this.iframeDocument.body.scrollTop = 0;\n        const rowEl = document.createElement(\"div\");\n        rowEl.classList.add(\"row\", \"g-0\", \"o_snippets_preview_row\");\n        rowEl.style.setProperty(\"direction\", this.props.frontendDirection);\n        const leftColEl = document.createElement(\"div\");\n        leftColEl.classList.add(\"col-lg-6\");\n        rowEl.appendChild(leftColEl);\n        const rightColEl = document.createElement(\"div\");\n        rightColEl.classList.add(\"col-lg-6\");\n        rowEl.appendChild(rightColEl);\n        this.iframeDocument.body.appendChild(rowEl);\n\n        // Split the next computation in chunks. A first big chunk of snippets\n        // to be computed first, then the rest in smaller chunks. This allows to\n        // make sure that a tab with many snippets (or a one letter search for\n        // instance) can show something filling the screen quickly without a\n        // laggy effect, and without waiting for the full computation. Splitting\n        // the rest in smaller chunks allow to cancel the loading quickly if a\n        // new search is entered.\n        const BIG_CHUNK_SIZE = 6;\n        const SMALL_CHUNK_SIZE = 3;\n        const chunks = [snippetsToDisplay.splice(0, BIG_CHUNK_SIZE)];\n        while (snippetsToDisplay.length) {\n            chunks.push(snippetsToDisplay.splice(0, SMALL_CHUNK_SIZE));\n        }\n        let leftColSize = 0;\n        let rightColSize = 0;\n        for (const chunk of chunks) {\n            // First compute all snippets UI and put them in the left column,\n            // invisible. Parallelize image loading and wait for it before\n            // sorting the items in the right column.\n            const itemEls = await Promise.all(chunk.map(snippet => {\n                let containerEl = null;\n\n                // Create cloned snippet.\n                let clonedSnippetEl;\n                let originalSnippet;\n                if (snippet.isCustom) {\n                    originalSnippet = [...this.props.snippets.values()].filter(snip =>\n                        !snip.isCustom && snip.name === snippet.name\n                    )[0];\n                    if (originalSnippet.baseBody.querySelector(\".s_dialog_preview\")\n                        || originalSnippet.imagePreview\n                        // Specific case for \"s_countdown\" because it's hybrid (also\n                        // inner content). TODO: It might be possible to have a real\n                        // preview for \"s_countdown\".\n                        || originalSnippet.name === \"s_countdown\") {\n                        clonedSnippetEl = originalSnippet.baseBody.cloneNode(true);\n                    }\n                }\n                if (!clonedSnippetEl) {\n                    clonedSnippetEl = snippet.baseBody.cloneNode(true);\n                }\n                clonedSnippetEl.classList.remove(\"oe_snippet_body\");\n                const snippetPreviewWrapEl = document.createElement(\"div\");\n                snippetPreviewWrapEl.classList.add(\"o_snippet_preview_wrap\", \"position-relative\");\n                snippetPreviewWrapEl.dataset.snippetId = snippet.name;\n                snippetPreviewWrapEl.dataset.snippetKey = snippet.key;\n                snippetPreviewWrapEl.appendChild(clonedSnippetEl);\n                this.__onSnippetPreviewClick = this._onSnippetPreviewClick.bind(this);\n                snippetPreviewWrapEl.addEventListener(\"click\", this.__onSnippetPreviewClick);\n                containerEl = snippetPreviewWrapEl;\n\n                // Add an \"Install\" button for installable snippets.\n                if (snippet.installable) {\n                    snippetPreviewWrapEl.classList.add(\"o_snippet_preview_install\");\n                    clonedSnippetEl.dataset.moduleId = snippet.moduleId;\n                    const installBtnEl = document.createElement(\"button\");\n                    installBtnEl.classList.add(\"o_snippet_preview_install_btn\", \"btn\", \"text-white\", \"rounded-1\", \"mx-auto\", \"p-2\", \"bottom-50\");\n                    installBtnEl.innerText = _t(\"Install %s\", snippet.displayName);\n                    snippetPreviewWrapEl.appendChild(installBtnEl);\n                }\n\n                // Replace the snippet with an image preview if one exists.\n                const imagePreview = snippet.imagePreview || originalSnippet?.imagePreview;\n                if (imagePreview) {\n                    // Enforce no-padding for image previews\n                    clonedSnippetEl.style.setProperty(\"padding\", \"0\", \"important\");\n                    const previewImgDivEl = document.createElement(\"div\");\n                    previewImgDivEl.classList.add(\"s_dialog_preview\", \"s_dialog_preview_image\");\n                    const previewImgEl = document.createElement(\"img\");\n                    previewImgEl.src = imagePreview;\n                    previewImgDivEl.appendChild(previewImgEl);\n                    clonedSnippetEl.innerHTML = \"\";\n                    clonedSnippetEl.appendChild(previewImgDivEl);\n                }\n\n                clonedSnippetEl.classList.remove(\"o_dynamic_empty\");\n\n                // Custom snippet.\n                if (snippet.isCustom) {\n                    const editCustomSnippetEl = document.createElement(\"div\");\n                    editCustomSnippetEl.classList.add(\"d-grid\", \"mt-2\", \"mx-5\", \"gap-2\",\n                        \"d-md-flex\", \"justify-content-md-end\", \"o_custom_snippet_edit\");\n\n                    const spanEl = document.createElement(\"span\");\n                    spanEl.classList.add(\"w-100\");\n                    spanEl.textContent = snippet.displayName;\n\n                    const renameBtnEl = document.createElement(\"button\");\n                    renameBtnEl.classList.add(\"btn\", \"fa\", \"fa-pencil\", \"me-md-2\");\n                    renameBtnEl.type = \"button\";\n\n                    const removeBtnEl = document.createElement(\"button\");\n                    removeBtnEl.classList.add(\"btn\", \"fa\", \"fa-trash\");\n                    removeBtnEl.type = \"button\";\n\n                    editCustomSnippetEl.appendChild(spanEl);\n                    editCustomSnippetEl.appendChild(renameBtnEl);\n                    editCustomSnippetEl.appendChild(removeBtnEl);\n\n                    const customSnippetWrapEl = document.createElement(\"div\");\n                    customSnippetWrapEl.classList.add(\"o_custom_snippet_wrap\");\n                    customSnippetWrapEl.appendChild(snippetPreviewWrapEl);\n                    customSnippetWrapEl.appendChild(editCustomSnippetEl);\n                    containerEl = customSnippetWrapEl;\n\n                    this.__onRenameCustomBtnClick = this._onRenameCustomBtnClick.bind(this);\n                    renameBtnEl.addEventListener(\"click\", this.__onRenameCustomBtnClick);\n                    this.__onDeleteCustomBtnClick = this._onDeleteCustomBtnClick.bind(this);\n                    removeBtnEl.addEventListener(\"click\", this.__onDeleteCustomBtnClick);\n                }\n\n                // Will be sorted in the right columns after\n                containerEl.classList.add(\"invisible\");\n                leftColEl.appendChild(containerEl);\n\n                // Await images.\n                const imageEls = snippetPreviewWrapEl.querySelectorAll(\"img\");\n                // TODO: move onceAllImagesLoaded in web_editor and to use it here\n                return Promise.all(Array.from(imageEls).map(imgEl => {\n                    imgEl.setAttribute(\"loading\", \"eager\");\n                    return new Promise(resolve => {\n                        if (imgEl.complete) {\n                            resolve();\n                        } else {\n                            imgEl.onload = () => resolve();\n                            // If the image could not be loaded, we still want the\n                            // \"d-none\" class to be removed.\n                            imgEl.onerror = () => resolve();\n                        }\n                    });\n                })).then(() => containerEl);\n            }));\n\n            // During the asynchronous period, another call to insertSnippets\n            // could have been made. In that case, we should just stop here.\n            if (this.currentInsertSnippetsCallID !== insertSnippetsCallID) {\n                return;\n            }\n\n            // Sort items in the right column based on their size and the\n            // currently computed size of the column. Note that this does not\n            // compute the size of the column after each element addition: this\n            // is wildly inefficient. Instead: compute the sizes, then add them\n            // all in one go into the right column.\n            const leftColElements = [];\n            const rightColElements = [];\n            for (const itemEl of itemEls) {\n                const size = itemEl.getBoundingClientRect().height;\n                if (leftColSize <= rightColSize) {\n                    leftColElements.push(itemEl);\n                    leftColSize += size;\n                } else {\n                    rightColElements.push(itemEl);\n                    rightColSize += size;\n                }\n            }\n            for (const [colEl, colItemEls] of [\n                [leftColEl, leftColElements],\n                [rightColEl, rightColElements],\n            ]) {\n                for (const el of colItemEls) {\n                    colEl.appendChild(el);\n                    el.classList.remove(\"invisible\");\n                }\n            }\n\n            // Only when the first chunk is loaded do we remove the previously\n            // loaded search.\n            while (rowEl.previousSibling) {\n                rowEl.previousSibling.remove();\n            }\n        }\n\n        this._updateSnippetContent(this.iframeDocument);\n    }\n    /**\n     * Inserts the style into the iframe's <head>.\n     */\n    async insertStyle() {\n        // Gets the HTML <link> tags of the website preview.\n        const pagePreviewIframeEl = document.querySelector(\".o_iframe\");\n        const cssLinkEls = pagePreviewIframeEl.contentDocument.head\n            .querySelectorAll(\"link[type='text/css']\");\n        // Inserts the the HTML <link> elements into the dialog's <iframe>.\n        const linkPromises = Array.from(cssLinkEls).map((cssLinkEl) => {\n            return new Promise((resolve) => {\n                const clonedLinkEl = cssLinkEl.cloneNode(true);\n                this.iframeDocument.head.appendChild(clonedLinkEl);\n                clonedLinkEl.onload = () => resolve();\n            });\n        });\n        // If the \"Page Layout\" option is not \"Full\" (e.g., \"PostCard\"), the\n        // <main> background color is used to define the \"--body-bg\" variable so\n        // that the snippet previews have the same background color as they\n        // would have if dropped into the page.\n        const mainEl = pagePreviewIframeEl.contentDocument.body.querySelector(\"#wrapwrap > main\");\n        const mainBgColor = mainEl && getComputedStyle(mainEl).backgroundColor;\n        if (mainBgColor !== \"rgba(0, 0, 0, 0)\") {\n            this.iframeDocument.body.style.setProperty(\"--body-bg\", mainBgColor);\n        }\n        await Promise.all(linkPromises);\n    }\n\n    _onSnippetPreviewClick(ev) {\n        let selectedSnippetEl = ev.currentTarget.querySelector(\"[data-name]\");\n        const snippetKey = parseInt(ev.currentTarget.dataset.snippetKey);\n        const moduleId = parseInt(selectedSnippetEl?.dataset.moduleId);\n        if (moduleId) {\n            this.props.installModule(moduleId, selectedSnippetEl.dataset.name);\n        } else {\n            selectedSnippetEl = this.props.snippets.get(snippetKey);\n            selectedSnippetEl = selectedSnippetEl.baseBody.cloneNode(true);\n            selectedSnippetEl.classList.remove(\"oe_snippet_body\");\n            const snippetDialogPreviews = selectedSnippetEl.querySelectorAll(\".s_dialog_preview\");\n            for (const snippetDialogPreview of snippetDialogPreviews) {\n                snippetDialogPreview.remove();\n            }\n            this.props.addSnippet(selectedSnippetEl);\n            // Adapt the snippet content right after adding it to the DOM.\n            this._updateSnippetContent(selectedSnippetEl);\n            this.props.close();\n        }\n    }\n\n    _onRenameCustomBtnClick(ev) {\n        const snippetKey = ev.currentTarget.closest(\".o_custom_snippet_wrap\")\n            .querySelector(\"[data-snippet-key]\").dataset.snippetKey;\n        const snippet = this.props.snippets.get(parseInt(snippetKey));\n        this.dialog.add(RenameCustomSnippetDialog, {\n            currentName: snippet.displayName,\n            confirm: async (newName) => {\n                this.props.renameCustomSnippet(parseInt(snippetKey), newName);\n            },\n        });\n    }\n\n    _onDeleteCustomBtnClick(ev) {\n        const snippetKey = ev.currentTarget.closest(\".o_custom_snippet_wrap\")\n            .querySelector(\"[data-snippet-key]\").dataset.snippetKey;\n        Promise.resolve(this.props.deleteCustomSnippet(parseInt(snippetKey))).then(() => {\n            // Remove the \"Custom\" tab if the last custom snippet was removed\n            const snippetsData = [...this.props.snippets.values()];\n            const stillHasCustom = !!snippetsData.find(snippet => snippet.group === \"custom\");\n            if (!stillHasCustom) {\n                this.snippetGroups = this.snippetGroups.filter(group => group.name !== \"custom\");\n                if (this.state.groupSelected === \"custom\") {\n                    this.state.groupSelected = this.snippetGroups[0].name;\n                }\n            }\n        });\n    }\n\n    /**\n     * Allows to update the snippets to build & adapt dynamic content.\n     *\n     * @private\n     */\n    _updateSnippetContent(snippetEl) {}\n}\n", "/** @odoo-module **/\n\n// jQuery extensions\n$.extend($.expr[':'], {\n    o_editable: function (node, i, m) {\n        while (node) {\n            if (node.className && typeof node.className === \"string\") {\n                if (node.className.indexOf('o_not_editable') !== -1) {\n                    return false;\n                }\n                if (node.className.indexOf('o_editable') !== -1) {\n                    return true;\n                }\n            }\n            node = node.parentNode;\n        }\n        return false;\n    },\n});\n\nfunction firstChild(node) {\n    while (node.firstChild) {\n        node = node.firstChild;\n    }\n    return node;\n}\nfunction lastChild(node) {\n    while (node.lastChild) {\n        node = node.lastChild;\n    }\n    return node;\n}\nfunction nodeLength(node) {\n    if (node.nodeType === Node.TEXT_NODE) {\n        return node.nodeValue.length;\n    } else {\n        return node.childNodes.length;\n    }\n}\n\n$.fn.extend({\n    focusIn: function () {\n        if (this.length) {\n            const selection = this[0].ownerDocument.getSelection();\n            selection.removeAllRanges();\n\n            const range = new Range();\n            const node = firstChild(this[0]);\n            range.setStart(node, 0);\n            range.setEnd(node, 0);\n            selection.addRange(range);\n        }\n        return this;\n    },\n    focusInEnd: function () {\n        if (this.length) {\n            const selection = this[0].ownerDocument.getSelection();\n            selection.removeAllRanges();\n\n            const range = new Range();\n            const node = lastChild(this[0]);\n            const length = nodeLength(node);\n\n            range.setStart(node, length);\n            range.setEnd(node, length);\n            selection.addRange(range);\n        }\n        return this;\n    },\n    selectContent: function () {\n        if (this.length && !this[0].hasChildNodes()) {\n            return this.selectElement();\n        }\n        if (this.length) {\n            const selection = this[0].ownerDocument.getSelection();\n            selection.removeAllRanges();\n\n            const range = new Range();\n            range.setStart(this[0].firstChild, 0);\n            range.setEnd(this[0].lastChild, this[0].lastChild.length);\n            selection.addRange(range);\n        }\n        return this;\n    },\n    selectElement: function () {\n        if (this.length) {\n            const selection = this[0].ownerDocument.getSelection();\n            selection.removeAllRanges();\n\n            const element = this[0];\n            const parent = element.parentNode;\n            const offsetStart = Array.from(parent.childNodes).indexOf(element);\n\n            const range = new Range();\n            range.setStart(parent, offsetStart);\n            range.setEnd(parent, offsetStart + 1);\n            selection.addRange(range);\n        }\n        return this;\n    },\n});\n", "/** @odoo-module **/\n\nimport { session } from \"@web/session\";\nimport { user } from \"@web/core/user\";\nimport { MediaDialog } from \"@web_editor/components/media_dialog/media_dialog\";\nimport { VideoSelector } from \"@web_editor/components/media_dialog/video_selector\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { useService } from \"@web/core/utils/hooks\";\nimport customColors from \"@web_editor/js/editor/custom_colors\";\nimport { localization } from \"@web/core/l10n/localization\";\nimport * as OdooEditorLib from \"@web_editor/js/editor/odoo-editor/src/OdooEditor\";\nimport { Toolbar } from \"@web_editor/js/editor/toolbar\";\nimport { LinkPopoverWidget } from '@web_editor/js/wysiwyg/widgets/link_popover_widget';\nimport { AltDialog } from '@web_editor/js/wysiwyg/widgets/alt_dialog';\nimport { ChatGPTPromptDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_prompt_dialog';\nimport { ChatGPTAlternativesDialog } from '@web_editor/js/wysiwyg/widgets/chatgpt_alternatives_dialog';\nimport { ChatGPTTranslateDialog } from \"@web_editor/js/wysiwyg/widgets/chatgpt_translate_dialog\";\nimport { ImageCrop } from '@web_editor/js/wysiwyg/widgets/image_crop';\n\nimport * as wysiwygUtils from \"@web_editor/js/common/wysiwyg_utils\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport { isIconElement, isSelectionInSelectors, peek } from '@web_editor/js/editor/odoo-editor/src/utils/utils';\nimport { PeerToPeer, RequestError } from \"@web_editor/js/wysiwyg/PeerToPeer\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { groupBy } from \"@web/core/utils/arrays\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { registry } from \"@web/core/registry\";\nimport { FileViewer } from \"@web/core/file_viewer/file_viewer\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { Deferred, Mutex } from \"@web/core/utils/concurrency\";\nimport { AlertDialog, ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ConflictDialog } from \"./conflict_dialog\";\nimport { getOrCreateLink } from \"./widgets/link\";\nimport { shouldUnlink } from '@web_editor/js/wysiwyg/widgets/link_tools';\nimport { LinkDialog } from \"./widgets/link_dialog\";\nimport {\n    Component,\n    EventBus,\n    useRef,\n    useState,\n    onWillStart,\n    onMounted,\n    onWillDestroy,\n    onWillUpdateProps,\n    markup,\n    status,\n} from \"@odoo/owl\";\nimport { isCSSColor } from '@web/core/utils/colors';\nimport { EmojiPicker } from '@web/core/emoji_picker/emoji_picker';\nimport { Tooltip } from \"@web/core/tooltip/tooltip\";\n\nconst OdooEditor = OdooEditorLib.OdooEditor;\nconst getDeepRange = OdooEditorLib.getDeepRange;\nconst getInSelection = OdooEditorLib.getInSelection;\nconst isProtected = OdooEditorLib.isProtected;\nconst rgbToHex = OdooEditorLib.rgbToHex;\nconst preserveCursor = OdooEditorLib.preserveCursor;\nconst closestElement = OdooEditorLib.closestElement;\nconst setSelection = OdooEditorLib.setSelection;\nconst endPos = OdooEditorLib.endPos;\nconst hasValidSelection = OdooEditorLib.hasValidSelection;\nconst parseHTML = OdooEditorLib.parseHTML;\nconst closestBlock = OdooEditorLib.closestBlock;\nconst getRangePosition = OdooEditorLib.getRangePosition;\nconst childNodeIndex = OdooEditorLib.childNodeIndex;\nconst fillEmpty = OdooEditorLib.fillEmpty;\nconst isVisible = OdooEditorLib.isVisible;\nconst getDeepestPosition = OdooEditorLib.getDeepestPosition;\nconst paragraphRelatedElements = OdooEditorLib.paragraphRelatedElements;\n\nfunction getJqueryFromDocument(doc) {\n    if (doc.defaultView && doc.defaultView.$) {\n        return doc.defaultView.$;\n    } else {\n        const _jquery = window.$;\n        return (...args) => {\n            if (args.length <= 2 && typeof args[0] === \"string\") {\n                return _jquery(args[0], args[1] || doc);\n            } else {\n                return _jquery(...args)\n            }\n        }\n    }\n}\n\nvar id = 0;\nconst basicMediaSelector = 'img, .fa, .o_image, .media_iframe_video';\n// (see isImageSupportedForStyle).\nconst mediaSelector = basicMediaSelector.split(',').map(s => `${s}:not([data-oe-xpath])`).join(',');\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.\nconst 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.\nconst CHECK_OFFLINE_TIME = 1000;\nconst PTP_CLIENT_DISCONNECTED_STATES = [\n    'failed',\n    'closed',\n    'disconnected',\n];\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\nlet fileViewerId = 0;\n\nexport class Wysiwyg extends Component {\n    static template = 'web_editor.Wysiwyg';\n    static components = { MediaDialog, VideoSelector, Toolbar, ImageCrop };\n    static props = {\n        options: Object,\n        startWysiwyg: { type: Function, optional: true },\n        editingValue: { type: String, optional: true },\n    };\n    elRef = useRef(\"el\");\n    toolbarRef = useRef(\"toolbar\");\n    imageCropRef = useRef(\"imageCrop\");\n    colorPalettesProps = {\n        text: useState({\n            resetTabCount: 0,\n        }),\n        background: useState({\n            resetTabCount: 0,\n        }),\n    }\n    imageCropProps = useState({\n        showCount: 0,\n        media: undefined,\n        mimetype: undefined,\n    });\n    state = useState({\n        linkToolProps: false,\n        showToolbar: true,\n        toolbarProps: {},\n        showSnippetsMenu: false,\n        snippetsMenuFolded: false,\n    });\n\n    setup() {\n        this.orm = useService('orm');\n        this.getColorPickerTemplateService = useService('get_color_picker_template');\n        this.notification = useService(\"notification\");\n        this.popover = useService(\"popover\");\n        this.busService = this.env.services.bus_service;\n        this.user = user;\n        this.snippetsMenuContainer = useRef(\"snippets-menu-container\");\n        this.mutex = new Mutex();\n        this.snippetsMenuBus = new EventBus();\n\n        const getColorPickedHandler = (colorType) => {\n            return (params) => {\n                if (this.hadNonCollapsedSelectionBeforeColorpicker) {\n                    this.odooEditor.historyResetLatestComputedSelection(true);\n                }\n                // Unstash the mutations now that the color is picked.\n                this.odooEditor.historyUnstash();\n                this._processAndApplyColor(colorType, params.color);\n                // Deselect tables so the applied color can be seen\n                // without using `!important` (otherwise the selection\n                // hides it).\n                if (hasValidSelection(this.odooEditor.editable)) {\n                    this.odooEditor.deselectTable();\n                }\n                this._updateEditorUI(this.lastMediaClicked && { target: this.lastMediaClicked });\n            };\n        }\n\n        const getColorHoverHandler = (colorType) => {\n            return (props) => {\n                if (this.hadNonCollapsedSelectionBeforeColorpicker) {\n                    this.odooEditor.historyResetLatestComputedSelection(true);\n                }\n                this.odooEditor.historyPauseSteps();\n                try {\n                    this._processAndApplyColor(colorType, props.color, true);\n                    this.odooEditor._computeHistorySelection();\n                } finally {\n                    this.odooEditor.historyUnpauseSteps();\n                }\n            }\n        };\n\n        const colorPaletteCommonOptions = {\n            excluded: ['transparent_grayscale'],\n            document: this.props.options.document,\n            selectedTab: 'theme-colors',\n            withGradients: true,\n            onColorLeave: () => {\n                // We need to prevent rollback in case the seclection is in unremovable\n                this.odooEditor.withoutRollback(() => this.odooEditor.historyRevertCurrentStep());\n                // Compute the selection to ensure it's preserved between\n                // selectionchange events in case this gets triggered multiple\n                // times quickly.\n                this.odooEditor._computeHistorySelection();\n            },\n            onInputEnter: (ev) => {\n                const pickergroup = ev.target.closest('.colorpicker-group');\n                $(pickergroup.querySelector('.dropdown-toggle')).dropdown('hide');\n            },\n\n            getTemplate: this.getColorpickerTemplate.bind(this),\n            getEditableCustomColors: () => {\n                if (!this.$editable) {\n                    return [];\n                }\n                return [...this.$editable[0].querySelectorAll('[style*=\"color\"]')].map(el => {\n                    return [el.style.color, el.style.backgroundColor];\n                }).flat();\n            },\n        };\n        onWillStart(() => {\n            this.init();\n\n            Object.assign(this.colorPalettesProps.text, colorPaletteCommonOptions, {\n                document: this.options.document,\n                onColorPicked: getColorPickedHandler('text'),\n                onCustomColorPicked: getColorPickedHandler('text'),\n                onColorHover: getColorHoverHandler('text'),\n                onColorpaletteTabChange: this.getColorPaletteTabChangeHandler('text').bind(this),\n            });\n            Object.assign(this.colorPalettesProps.background, colorPaletteCommonOptions, {\n                document: this.options.document,\n                onColorPicked: getColorPickedHandler('background'),\n                onCustomColorPicked: getColorPickedHandler('background'),\n                onColorHover: getColorHoverHandler('background'),\n                onColorpaletteTabChange: this.getColorPaletteTabChangeHandler('background').bind(this),\n            });\n\n            this._setToolbarProps();\n        });\n        onMounted(async () => {\n            this.el = this.elRef.el\n            this.$el = $(this.elRef.el);\n            this._renderElement();\n            if (this.props.startWysiwyg) {\n                await this.props.startWysiwyg(this);\n            } else {\n                await this.startEdition();\n            }\n        });\n        onWillDestroy(() => {\n            this.destroy();\n        });\n        onWillUpdateProps((newProps) => {\n            this.options = this._getEditorOptions(newProps.options);\n            this._setToolbarProps();\n\n            const lastValue = String(this.props.options.value || '');\n            const lastRecordInfo = this.props.options.recordInfo;\n            const lastCollaborationChannel = this.props.options.collaborationChannel;\n            const newValue = String(newProps.options.value || '');\n            const newRecordInfo = newProps.options.recordInfo;\n            const newCollaborationChannel = newProps.options.collaborationChannel;\n\n            const isDifferentRecord =\n                JSON.stringify(lastRecordInfo) !== JSON.stringify(newRecordInfo) ||\n                JSON.stringify(lastCollaborationChannel) !== JSON.stringify(newCollaborationChannel);\n            const isDiscardedRecord = !isDifferentRecord && newProps.options.record && !newProps.options.record.dirty;\n\n            if (\n                (\n                    stripHistoryIds(newValue) !== stripHistoryIds(newProps.editingValue) &&\n                    stripHistoryIds(lastValue) !== stripHistoryIds(newValue)\n                ) ||\n                    isDifferentRecord\n                )\n            {\n                if (isDifferentRecord || isDiscardedRecord) {\n                    this.resetEditor(newValue, newProps.options);\n                } else {\n                    this.setValue(newValue);\n                }\n                this.env.onWysiwygReset && this.env.onWysiwygReset();\n            }\n        });\n    }\n\n    defaultOptions = {\n        lang: 'odoo',\n        colors: customColors,\n        recordInfo: {context: {}},\n        document: document,\n        allowCommandVideo: true,\n        allowCommandImage: true,\n        allowCommandLink: true,\n        insertParagraphAfterColumns: true,\n        onHistoryResetFromSteps: () => {},\n        autostart: true,\n        dropImageAsAttachment: true,\n        editorPlugins: [],\n        useResponsiveFontSizes: true,\n        showResponsiveFontSizesBadges: false,\n        showExtendedTextStylesOptions: false,\n        getCSSVariableValue: weUtils.getCSSVariableValue,\n        convertNumericToUnit: weUtils.convertNumericToUnit,\n    };\n    init() {\n        this.id = ++id;\n        this.options = this._getEditorOptions(this.props.options);\n        this.saving_mutex = new Mutex();\n        // Keeps track of color palettes per event name.\n        this.colorpickers = {};\n        this._onDocumentMousedown = this._onDocumentMousedown.bind(this);\n        this._onBlur = this._onBlur.bind(this);\n        this._onScroll = this._onScroll.bind(this);\n        this.customizableLinksSelector = 'a'\n            + ':not([data-bs-toggle=\"tab\"])'\n            + ':not([data-bs-toggle=\"collapse\"])'\n            + ':not([data-bs-toggle=\"dropdown\"])'\n            + ':not(.dropdown-item)';\n        // navigator.onLine is sometimes a false positive, this._isOnline use\n        // more heuristics to bypass the limitation.\n        this._isOnline = true;\n        this._signalOnline = this._signalOnline.bind(this);\n        this.tooltipTimeouts = [];\n        Wysiwyg.activeWysiwygs.add(this);\n        this._joinPeerToPeer = this._joinPeerToPeer.bind(this);\n    }\n    /**\n     *\n     * @override\n     */\n    async start() {\n        // If this widget is started from the OWL legacy component, at the time\n        // of start, the $el is not in the document yet. Some instruction in the\n        // start rely on the $el being in the document at that time, including\n        // code for the collaboration (for adding cursors) or the iframe loading\n        // in mass_mailing.\n        if (this.options.autostart) {\n            return this.startEdition();\n        }\n    }\n    async startEdition() {\n        const self = this;\n\n        const options = this.options;\n\n        this.$editable ??= this.$el;\n        if (options.value) {\n            this.$editable.html(options.value);\n        }\n\n        this._isDocumentStale = 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        // 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        // 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 = options.value && this._getLastHistoryStepId(options.value);\n\n        this.$editable.data('wysiwyg', this);\n        this.$editable.data('oe-model', options.recordInfo.res_model);\n        this.$editable.data('oe-id', options.recordInfo.res_id);\n        document.addEventListener('mousedown', this._onDocumentMousedown, true);\n        this._bindOnBlur();\n\n        this.toolbarEl = this.toolbarRef.el.firstChild;\n\n        this.imageCropEL = this.imageCropRef.el.firstChild;\n        options.document.body.append(this.imageCropEL);\n\n        const powerboxOptions = this._getPowerboxOptions();\n\n        let editorCollaborationOptions;\n        if (this._isCollaborationEnabled(options)) {\n            this._currentClientId = this._generateClientId();\n            editorCollaborationOptions = this.setupCollaboration(options.collaborationChannel);\n            if (this.options.collaborativeTrigger === 'start') {\n                this._joinPeerToPeer();\n            } else if (this.options.collaborativeTrigger === 'focus') {\n                // Wait until editor is focused to join the peer to peer network.\n                this.$editable[0].addEventListener('focus', this._joinPeerToPeer);\n            }\n        }\n\n        const getYoutubeVideoElement = async (url) => {\n            const { embed_url: src } = await this._serviceRpc(\n                '/web_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        weUtils.setEditableDocument(this.options.document);\n\n        const _getContentEditableAreas = this.options.getContentEditableAreas;\n        this.odooEditor = new OdooEditor(this.$editable[0], Object.assign({\n            _t: _t,\n            toolbar: this.toolbarEl,\n            document: this.options.document,\n            autohideToolbar: !!this.options.autohideToolbar,\n            isRootEditable: this.options.isRootEditable,\n            onPostSanitize: this._onPostSanitize.bind(this),\n            placeholder: this.options.placeholder,\n            powerboxFilters: this.options.powerboxFilters || [],\n            showEmptyElementHint: this.options.showEmptyElementHint,\n            controlHistoryFromDocument: this.options.controlHistoryFromDocument,\n            initialHistoryId: this._serverLastStepId,\n            // TODO other places in this file call this.options.getContentEditableAreas\n            // without the extension here. It does not seem to be a problem (it\n            // was like that before o_editor_banner elements were considered\n            // here), but we might want to review that.\n            getContentEditableAreas: (...args) => {\n                const areaEls = _getContentEditableAreas?.(...args) || [];\n                const bannerEls = this.$editable[0].querySelectorAll('.o_editor_banner > div');\n                return [...areaEls, ...bannerEls];\n            },\n            getReadOnlyAreas: this.options.getReadOnlyAreas,\n            getUnremovableElements: this.options.getUnremovableElements,\n            defaultLinkAttributes: this.options.userGeneratedContent ? {rel: 'ugc' } : {},\n            allowCommandVideo: this.options.allowCommandVideo,\n            allowInlineAtRoot: this.options.allowInlineAtRoot,\n            getYoutubeVideoElement: getYoutubeVideoElement,\n            getContextFromParentRect: options.getContextFromParentRect,\n            getScrollContainerRect: () => {\n                if (!this.scrollContainer || !this.scrollContainer.getBoundingClientRect) {\n                    this.scrollContainer = document.querySelector('.o_action_manager') || document.body;\n                }\n                return this.scrollContainer.getBoundingClientRect();\n            },\n            getPowerboxElement: () => {\n                const selection = (this.options.document || document).getSelection();\n                if (selection.isCollapsed && selection.rangeCount) {\n                    const [deepestNode] = getDeepestPosition(selection.anchorNode, selection.anchorOffset);\n                    const elementSelectors = [\n                        'LI:not([t-field])',\n                        \"DIV:not([t-field]):not(.o_not_editable):not([contenteditable='false'])\",\n                        ...paragraphRelatedElements.map(tag => `${tag}:not([t-field])`),\n                    ];\n                    const baseNode = closestElement(deepestNode, elementSelectors.join(', '));\n                    const fieldContainer = closestElement(deepestNode, '[data-oe-field]');\n                    if (!baseNode ||\n                        (\n                            fieldContainer &&\n                            !(\n                                fieldContainer.getAttribute('data-oe-field') === 'arch' ||\n                                fieldContainer.getAttribute('data-oe-type') === 'html'\n                            )\n                        )) {\n                        return false;\n                    }\n                    return baseNode;\n                }\n            },\n            isHintBlacklisted: node => {\n                return (node.classList && node.classList.contains('nav-item')) || (\n                    node.hasAttribute && (\n                        node.hasAttribute('data-target') ||\n                        node.hasAttribute('data-oe-model')\n                    )\n                );\n            },\n            filterMutationRecords: (records) => {\n                return records.filter((record) => {\n                    if (record.type === 'attributes'\n                            && record.attributeName === 'aria-describedby') {\n                        const value = (record.oldValue || record.target.getAttribute(record.attributeName));\n                        if (value && ['popover', 'tooltip'].some(type => value.startsWith(type))) {\n                            // E.g. prevents to consider the mutation due to\n                            // the 'o_edit_menu_popover' popover being shown.\n                            return false;\n                        }\n                    }\n                    return !(\n                        // TODO should probably not check o_header_standard\n                        // here, since it is a website class ?\n                        (record.target.classList && record.target.classList.contains('o_header_standard')) ||\n                        (record.type === 'attributes' && record.attributeName === 'data-last-history-steps')\n                    );\n                });\n            },\n            preHistoryUndo: () => {\n                this.destroyLinkTools();\n            },\n            beforeAnyCommand: this._beforeAnyCommand.bind(this),\n            commands: powerboxOptions.commands,\n            categories: powerboxOptions.categories,\n            plugins: options.editorPlugins,\n            direction: options.direction || localization.direction || 'ltr',\n            collaborationClientAvatarUrl: this._getCollaborationClientAvatarUrl(),\n            renderingClasses: [\"o_dirty\", \"o_transform_removal\", \"oe_edited_link\", \"o_menu_loading\", \"o_draggable\", \"o_link_in_selection\"],\n            dropImageAsAttachment: options.dropImageAsAttachment,\n            foldSnippets: !!options.foldSnippets,\n            useResponsiveFontSizes: options.useResponsiveFontSizes,\n            showResponsiveFontSizesBadges: options.showResponsiveFontSizesBadges,\n            showExtendedTextStylesOptions: options.showExtendedTextStylesOptions,\n            getCSSVariableValue: options.getCSSVariableValue,\n            convertNumericToUnit: options.convertNumericToUnit,\n            autoActivateContentEditable: this.options.autoActivateContentEditable,\n        }, editorCollaborationOptions));\n\n        this.odooEditor.addEventListener('contentChanged', function () {\n            self.$editable.trigger('content_changed');\n        });\n        document.addEventListener(\"mousemove\", this._signalOnline, true);\n        document.addEventListener(\"keydown\", this._signalOnline, true);\n        document.addEventListener(\"keyup\", this._signalOnline, true);\n        if (this.odooEditor.document !== document) {\n            this.odooEditor.document.addEventListener(\"mousemove\", this._signalOnline, true);\n            this.odooEditor.document.addEventListener(\"keydown\", this._signalOnline, true);\n            this.odooEditor.document.addEventListener(\"keyup\", this._signalOnline, true);\n        }\n\n        this._initialValue = this.getValue();\n\n        // TODO this code should be reviewed. Previously, it searched for the\n        // wrapwrap and added some scroll handler if found... we are now\n        // continuing to search for it, but it is not clear why. As the wrapwrap\n        // will be removed, this code will not stay for too long anyway.\n        if ($('wrapwrap').length) {\n            this.multiSelectionTarget = $().getScrollingTarget(this.odooEditor.document);\n            this.multiSelectionTarget.addEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true });\n        }\n\n        this.$editable.on('click', '[data-oe-field][data-oe-sanitize-prevent-edition]', () => {\n            this.env.services.dialog.add(AlertDialog, {\n                body: _t(\"Someone with escalated rights previously modified this area, you are therefore not able to modify it yourself.\"),\n            });\n        });\n\n        for (const field of this.$editable[0].querySelectorAll('[data-oe-type=\"text\"], [data-oe-type=\"char\"]')) {\n            if (!isVisible(field)) {\n                fillEmpty(field);\n            }\n        }\n\n        this._observeOdooFieldChanges();\n        this.$editable.on(\n            'mousedown touchstart',\n            '[data-oe-field]',\n            function () {\n                self.odooEditor.observerUnactive();\n                const $field = $(this);\n                if (($field.data('oe-type') === \"datetime\" || $field.data('oe-type') === \"date\")) {\n                    let selector = '[data-oe-id=\"' + $field.data('oe-id') + '\"]';\n                    selector += '[data-oe-field=\"' + $field.data('oe-field') + '\"]';\n                    selector += '[data-oe-model=\"' + $field.data('oe-model') + '\"]';\n                    const $linkedFieldNodes = self.$editable.find(selector).addBack(selector);\n                    $linkedFieldNodes.addClass('o_editable_date_field_linked');\n                    if (!$field.hasClass('o_editable_date_field_format_changed')) {\n                        $linkedFieldNodes.text($field.data('oe-original-with-format'));\n                        $linkedFieldNodes.addClass('o_editable_date_field_format_changed');\n                        $linkedFieldNodes.filter('.oe_hide_on_date_edit').addClass('d-none');\n                        setTimeout(() => {\n                            // we might hide the clicked date, focus the one\n                            // supposed to be editable\n                            Wysiwyg.setRange($linkedFieldNodes.filter(':not(.oe_hide_on_date_edit)')[0]);\n                        }, 0);\n                    }\n                }\n                if ($field.attr('contenteditable') !== 'false') {\n                    if ($field.data('oe-type') === \"monetary\") {\n                        $field.attr('contenteditable', false);\n                        const $currencyValue = $field.find('.oe_currency_value');\n                        $currencyValue.attr('contenteditable', true);\n                        $currencyValue.one('mouseup touchend', (e) => {\n                            $currencyValue.selectContent();\n                        });\n                    }\n                    if ($field.data('oe-type') === \"image\") {\n                        $field.attr('contenteditable', false);\n                        $field.find('img').attr('contenteditable', $field.data('oe-readonly') !== 1);\n                    }\n                    if ($field.is('[data-oe-many2one-id]')) {\n                        $field.attr('contenteditable', false);\n                    }\n                }\n                self.odooEditor.observerActive();\n            }\n        );\n\n        this.$editable.on('click', '.o_image, .media_iframe_video', e => e.preventDefault());\n\n        let closeBannerEmojiPicker;\n        this.$editable.on('click', '.o_editor_banner_icon', event => {\n            if (closeBannerEmojiPicker) {\n                closeBannerEmojiPicker();\n            }\n            closeBannerEmojiPicker = this.popover.add(event.target, EmojiPicker, {\n                onSelect: emoji => {\n                    event.target.innerText = emoji;\n                }\n            }, { position: 'bottom' });\n        });\n\n        this.showTooltip = true;\n        this.$editable.on('dblclick', mediaSelector, ev => {\n            const targetEl = ev.currentTarget;\n            let isEditable =\n                // TODO that first check is probably useless/wrong: checking if\n                // the media itself has editable content should not be relevant.\n                // In fact the content of all media should be marked as non\n                // editable anyway.\n                targetEl.isContentEditable ||\n                // For a media to be editable, the base case is to be in a\n                // container whose content is editable.\n                (targetEl.parentElement && targetEl.parentElement.isContentEditable);\n\n            if (!isEditable && targetEl.classList.contains('o_editable_media')) {\n                isEditable = weUtils.shouldEditableMediaBeEditable(targetEl);\n            }\n\n            if (isEditable) {\n                this.showTooltip = false;\n\n                if (!isProtected(this.odooEditor.document.getSelection().anchorNode)) {\n                    if (this.options.onDblClickEditableMedia && targetEl.nodeName === 'IMG' && targetEl.src) {\n                        this.options.onDblClickEditableMedia(ev);\n                    } else {\n                        this._onDblClickEditableMedia(ev);\n                    }\n                }\n            }\n        });\n\n        if (options.snippets) {\n            this.snippetsMenuComponent = await this.getSnippetsMenuClass(options);\n            await this._insertSnippetMenu();\n            $(this.odooEditor.document.body).addClass('editor_enable');\n\n            this._onBeforeUnload = (event) => {\n                if (this.isDirty()) {\n                    event.returnValue = _t('This document is not saved!');\n                }\n            };\n            window.addEventListener('beforeunload', this._onBeforeUnload);\n        }\n        if (this.options.getContentEditableAreas) {\n            $(this.options.getContentEditableAreas(this.odooEditor)).find('*').off('mousedown mouseup click');\n        }\n\n        // The toolbar must be configured after the snippetMenu is loaded\n        // because if snippetMenu is loaded in an iframe, binding of the color\n        // buttons must use the jquery loaded in that iframe.\n        this._configureToolbar(options);\n\n        $(this.odooEditor.editable).on('mouseup', this._updateEditorUI.bind(this));\n        $(this.odooEditor.editable).on('keydown', this._updateEditorUI.bind(this));\n        $(this.odooEditor.editable).on('keydown', this._handleShortcuts.bind(this));\n        // Ensure the Toolbar always have the correct layout in note.\n        this._updateEditorUI();\n\n        this.$editable.on('click.Wysiwyg', (ev) => {\n            const $target = $(ev.target).closest('a');\n\n            // Keep popover open if clicked inside it, but not on a button\n            if ($(ev.target).parents('.o_edit_menu_popover').length && !$target.length) {\n                ev.preventDefault();\n            }\n\n            if ($target.is(this.customizableLinksSelector)\n                    && $target.is('a')\n                    && $target[0].isContentEditable\n                    && !$target.attr('data-oe-model')\n                    && !$target.find('> [data-oe-model]').length\n                    && !$target[0].closest('.o_extra_menu_items')\n                    && $target[0].isContentEditable) {\n                if (ev.ctrlKey || ev.metaKey) {\n                    window.open($target[0].href, '_blank');\n                }\n                this.linkPopover = $target.data('popover-widget-initialized');\n                if (!this.linkPopover) {\n                    // TODO this code is ugly maybe the mutex should be in the\n                    // editor root widget / the popover should not depend on\n                    // editor panel (like originally intended but...) / ...\n                    (async () => {\n                        let container;\n                        if (this.state.showSnippetsMenu) {\n                            // Await for the editor panel to be fully updated\n                            // as some buttons of the link popover we create\n                            // here relies on clicking in that editor panel...\n                            await this.mutex.exec(() => null);\n                            container = this.options.document.getElementById('oe_manipulators');\n                        }\n                        this.linkPopover = LinkPopoverWidget.createFor({\n                            target: $target[0],\n                            wysiwyg: this,\n                            container,\n                            notify: (message, params) => {\n                                this.notification.add(message, { type: params.type });\n                            },\n                        });;\n                        $target.data('popover-widget-initialized', this.linkPopover);\n                    })();\n                }\n                // Setting the focus on the closest contenteditable element\n                // resets the selection inside that element if no selection\n                // exists.\n                $target.closest('[contenteditable=true]').focus();\n                if ($target.closest('#wrapwrap').length && this.state.showSnippetsMenu) {\n                    this.toggleLinkTools({\n                        forceOpen: true,\n                        link: $target[0],\n                        shouldFocusUrl: ev.detail !== 1,\n                    });\n                }\n            }\n        });\n\n        this._onSelectionChange = this._onSelectionChange.bind(this);\n        this.odooEditor.document.addEventListener('selectionchange', this._onSelectionChange);\n        if (!this.state.showSnippetsMenu) {\n            this.setCSSVariables(this.toolbarEl);\n        }\n\n        this.odooEditor.addEventListener('preObserverActive', () => {\n            // The onPostSanitize will be called right after the\n            // editor sanitization (to be right before the historyStep).\n            // If any `.o_not_editable` is created while the observer is\n            // unactive, now is the time to call `onPostSanitize` before the\n            // editor could register a mutation.\n            this._onPostSanitize(this.odooEditor.editable);\n        });\n\n        if (this.options.autohideToolbar) {\n            if (this.odooEditor.isMobile) {\n                this.odooEditor.editable.before(this.toolbarEl);\n            } else {\n                document.body.append(this.toolbarEl);\n            }\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            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        this.busService.subscribe('editor_collaboration', collaborationBusListener);\n        this.busService.addChannel(this._collaborationChannelName);\n        this._collaborationStopBus = () => {\n            Wysiwyg.activeCollaborationChannelNames.delete(this._collaborationChannelName);\n            this.busService.unsubscribe('editor_collaboration', collaborationBusListener);\n            this.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 clientsInfos = Object.values(this.ptp.clientsInfos);\n            const couldBeDisconnected =\n                Boolean(clientsInfos.length) &&\n                clientsInfos.every((x) => PTP_CLIENT_DISCONNECTED_STATES.includes(x.peerConnection && x.peerConnection.connectionState));\n\n            if (couldBeDisconnected) {\n                this._offlineTimeout = setTimeout(() => {\n                    this._signalOffline();\n                }, CONSIDER_OFFLINE_TIME);\n            }\n        }, CHECK_OFFLINE_TIME);\n\n        this._peerToPeerLoading = new Promise(async (resolve) => {\n            if (!ICE_SERVERS) {\n                ICE_SERVERS = await this._serviceRpc('/web_editor/get_ice_servers');\n            }\n            let iceServers = ICE_SERVERS;\n            if (!iceServers.length) {\n                iceServers = [\n                    {\n                        urls: [\n                            'stun:stun1.l.google.com:19302',\n                            'stun:stun2.l.google.com:19302',\n                        ],\n                    }\n                ];\n            }\n            this._iceServers = iceServers;\n\n            this.ptp = this._getNewPtp();\n\n            resolve();\n        });\n\n        const editorCollaborationOptions = {\n            collaborationClientId: this._currentClientId,\n            onHistoryStep: (historyStep) => {\n                if (!this.ptp) return;\n                this.ptp.notifyAllClients('oe_history_step', historyStep, { transport: 'rtc' });\n            },\n            onCollaborativeSelectionChange: debounce((collaborativeSelection) => {\n                if (!this.ptp) return;\n                this.ptp.notifyAllClients('oe_history_set_selection', collaborativeSelection, { transport: 'rtc' });\n            }, 50),\n            onHistoryMissingParentSteps: async ({ step, fromStepId }) => {\n                if (!this.ptp) return;\n                const missingSteps = await this.requestClient(\n                    step.clientId,\n                    'get_missing_steps', {\n                        fromStepId: fromStepId,\n                        toStepId: step.id\n                    },\n                    { transport: 'rtc' }\n                );\n                if (missingSteps === REQUEST_ERROR) return;\n                this._processMissingSteps(Array.isArray(missingSteps) ? missingSteps.concat(step) : missingSteps);\n            },\n        };\n        if (this.options.postProcessExternalSteps) {\n            editorCollaborationOptions.postProcessExternalSteps = this.options.postProcessExternalSteps;\n        }\n        return editorCollaborationOptions;\n    }\n    setupToolbar(toolbarEl) {\n        this.toolbarEl = toolbarEl;\n        this.odooEditor.setupToolbar(toolbarEl);\n        this._configureToolbar(this.options)\n        this._updateEditorUI();\n    }\n    /**\n     * @override\n     */\n    destroy() {\n        // Sometimes, the component is started and destroyed so quickly that\n        // external calls to `wysiwyg.getColorPickerTemplateService()` fail by\n        // the time it's done, even though `wysiwyg` was properly instantiated.\n        // As it's not needed once the component is destroyed, we return null.\n        this.getColorPickerTemplateService = () => null;\n        Wysiwyg.activeWysiwygs.delete(this);\n\n        this._stopPeerToPeer();\n        document.removeEventListener(\"mousemove\", this._signalOnline, true);\n        document.removeEventListener(\"keydown\", this._signalOnline, true);\n        document.removeEventListener(\"keyup\", this._signalOnline, true);\n        this._collaborationStopBus && this._collaborationStopBus();\n        if (this.odooEditor) {\n            this.odooEditor.document.removeEventListener(\"mousemove\", this._signalOnline, true);\n            this.odooEditor.document.removeEventListener(\"keydown\", this._signalOnline, true);\n            this.odooEditor.document.removeEventListener(\"keyup\", this._signalOnline, true);\n            this.odooEditor.document.removeEventListener('selectionchange', this._onSelectionChange);\n            this.odooEditor.destroy();\n        }\n        // If peer to peer is initializing, wait for properly closing it.\n        if (this._peerToPeerLoading) {\n            this._peerToPeerLoading.then(()=> {\n                this._stopPeerToPeer();\n                this._collaborationStopBus && this._collaborationStopBus();\n            });\n        }\n        clearInterval(this._collaborationInterval);\n        this.$editable && this.$editable.off('blur', this._onBlur);\n        document.removeEventListener('mousedown', this._onDocumentMousedown, true);\n        const $body = $(document.body);\n        $body.off('mousemove', this.resizerMousemove);\n        $body.off('mouseup', this.resizerMouseup);\n        this.multiSelectionTarget?.removeEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true });\n        this.$editable?.off('.Wysiwyg');\n        this.toolbarEl?.remove();\n        this.imageCropEL?.remove();\n        if (this.linkPopover) {\n            this.linkPopover.hide();\n        }\n        if (this._checkConnectionChange) {\n            window.removeEventListener('online', this._checkConnectionChange);\n            window.removeEventListener('offline', this._checkConnectionChange);\n        }\n        window.removeEventListener('beforeunload', this._onBeforeUnload);\n        for (const timeout of this.tooltipTimeouts) {\n            clearTimeout(timeout);\n        }\n        document.removeEventListener('scroll', this._onScroll, true);\n    }\n    /**\n     * @override\n     */\n    _renderElement() {\n        this.$editable = this.options.editable || $('<div class=\"note-editable\">');\n\n        // We add the field's name as id so default_focus will target it if\n        // needed. For that to work, it has to already be editable but note that\n        // the editor is at this point not yet instantiated.\n        if (typeof this.options.fieldId !== 'undefined' && !this.options.inIframe) {\n            this.$editable.attr('id', this.options.fieldId);\n            this.$editable.attr('contenteditable', true);\n        }\n\n        if (this.options.height) {\n            this.$editable.height(this.options.height);\n        }\n        if (this.options.minHeight) {\n            this.$editable.css('min-height', this.options.minHeight);\n        }\n        if (this.options.maxHeight && this.options.maxHeight > 10) {\n            this.$editable.css('max-height', this.options.maxHeight);\n        }\n        if (this.options.resizable && !isMobileOS()) {\n            const $wrapper = $('<div class=\"o_wysiwyg_wrapper odoo-editor\">');\n            $wrapper.append(this.$editable);\n            this.$resizer = $(`<div class=\"o_wysiwyg_resizer\">\n                <div class=\"o_wysiwyg_resizer_hook\"></div>\n                <div class=\"o_wysiwyg_resizer_hook\"></div>\n                <div class=\"o_wysiwyg_resizer_hook\"></div>\n            </div>`);\n            $wrapper.append(this.$resizer);\n            this._replaceElement($wrapper);\n\n            const minHeight = this.options.minHeight || 100;\n            this.$editable.height(this.options.height || minHeight);\n\n            // resizer hooks\n            let startOffsetTop;\n            let startHeight;\n            const $body = $(document.body);\n            const resizerMousedown = (e) => {\n                e.preventDefault();\n                e.stopPropagation();\n                $body.on('mousemove', this.resizerMousemove);\n                $body.on('mouseup', this.resizerMouseup);\n                startHeight = this.$editable.height();\n                startOffsetTop = e.pageY;\n            };\n            this.resizerMousemove = (e) => {\n                const offsetTop = e.pageY - startOffsetTop;\n                let height = startHeight + offsetTop;\n                if (height < minHeight) {\n                    height = minHeight;\n                }\n                this.$editable.height(height);\n            };\n            this.resizerMouseup = () => {\n                $body.off('mousemove', this.resizerMousemove);\n                $body.off('mouseup', this.resizerMouseup);\n            };\n            this.$resizer.on('mousedown', resizerMousedown);\n        } else {\n            if (!this.options.sideAttach) {\n                this._replaceElement(this.$editable);\n            }\n        }\n    }\n    /**\n     * @private\n     */\n    _replaceElement($el) {\n        this.el.replaceWith($el[0]);\n        this.el = $el[0];\n        this.$el = $el;\n    }\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n    /**\n     * Return the editable area.\n     *\n     * @returns {jQuery}\n     */\n    getEditable() {\n        return this.$editable;\n    }\n    /**\n     * Return true if the content has changed.\n     *\n     * @returns {Boolean}\n     */\n    isDirty() {\n        // TODO review... o_dirty is not even a set up system in web_editor,\n        // only in website... although some other code checks that class in\n        // web_editor for no apparent reason either. Also, why comparing HTML\n        // values if already confirmed dirty with the first check?\n        const isDocumentDirty = this.$editable[0].ownerDocument.defaultView.$(\".o_dirty\").length;\n        return this._initialValue !== (this.getValue() || this.$editable.val()) && isDocumentDirty;\n    }\n    /**\n     * Get the value of the editable element.\n     *\n     * @param {object} [options]\n     * @param {jQuery} [options.$layout]\n     * @returns {String}\n     */\n    getValue(options) {\n        var $editable = options && options.$layout || this.$editable.clone();\n        $editable.find('[contenteditable]').removeAttr('contenteditable');\n        $editable.find('[class=\"\"]').removeAttr('class');\n        $editable.find('[style=\"\"]').removeAttr('style');\n        $editable.find('[title=\"\"]').removeAttr('title');\n        $editable.find('[alt=\"\"]').removeAttr('alt');\n        $editable.find('[data-bs-original-title=\"\"]').removeAttr('data-bs-original-title');\n        $editable.find('[data-editor-message]').removeAttr('data-editor-message');\n        $editable.find('a.o_image, span.fa, i.fa').html('');\n        $editable.find('[aria-describedby]').removeAttr('aria-describedby').removeAttr('data-bs-original-title');\n        if (this.odooEditor) {\n            this.odooEditor.cleanForSave($editable[0]);\n            this._attachHistoryIds($editable[0]);\n        }\n        return $editable.html();\n    }\n    /**\n     * Save the content in the target\n     *      - in init option beforeSave\n     *      - receive editable jQuery DOM as attribute\n     *      - called after deactivate codeview if needed\n     * @returns {Promise}\n     *      - resolve with true if the content was dirty\n     */\n    save() {\n        const isDirty = this.isDirty();\n        const html = this.getValue();\n        if (this.$editable.is('textarea')) {\n            this.$editable.val(html);\n        } else {\n            this.$editable.html(html);\n        }\n        return Promise.resolve({isDirty: isDirty, html: html});\n    }\n    /**\n     * Reset the history.\n     */\n    historyReset() {\n        this.odooEditor.historyReset();\n    }\n    /**\n     * Saves the content or the given editable.\n     *\n     * @param {boolean} [reload=true]\n     * @param {Object} [editable=false] Specific editable to save\n     * @returns {Promise}\n     */\n    async saveContent(reload = true, editable = false) {\n        this.savingContent = true;\n        if (!editable) {\n            await this.cleanForSave();\n            const editables = \"getContentEditableAreas\" in this.options ? this.options.getContentEditableAreas(this.odooEditor) : [];\n            await this.savePendingImages(editables.length ? $(editables) : this.$editable);\n            await this._saveViewBlocks();\n        } else {\n            await this.cleanForSave(editable);\n            await this.savePendingImages(editable);\n            await this._saveViewBlocks(false, editable);\n        }\n\n        this.savingContent = false;\n\n        window.removeEventListener('beforeunload', this._onBeforeUnload);\n        if (reload) {\n            window.location.reload();\n        }\n    }\n    /**\n     * Checks if the Wysiwyg is currently saving content. It can be used to\n     * prevent some unwanted actions during save.\n     *\n     * @returns {Boolean}\n     */\n    isSaving() {\n        return !!this.savingContent;\n    }\n    /**\n     * Asks the user if he really wants to discard its changes (if there are\n     * some of them), then simply reload the page if he wants to.\n     *\n     * @param {boolean} [reload=true]\n     *        true if the page has to be reloaded when the user answers yes\n     *        (do nothing otherwise but add this to allow class extension)\n     * @returns {Promise}\n     */\n    cancel(reload) {\n        var self = this;\n        return new Promise((resolve, reject) => {\n            this.env.services.dialog.add(ConfirmationDialog, {\n                body: _t(\"If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode.\"),\n                confirm: () => resolve(),\n                cancel: () => reject()\n            });\n        }).then(function () {\n            if (reload !== false) {\n                window.onbeforeunload = null;\n                return self._reload();\n            }\n        });\n    }\n    /**\n     * Create/Update attachments for unsaved images.\n     * (e.g. modified/cropped images, drag & dropped images, pasted images..)\n     *\n     * @param {jQuery} $editable\n     * @returns {Promise}\n     */\n    savePendingImages($editable = this.$editable) {\n        const defs = Array.from($editable).map(async (editableEl) => {\n            const { resModel, resId } = this._getRecordInfo(editableEl);\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(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            const modifiedProms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(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            return Promise.all([...b64Proms, ...modifiedProms]);\n        });\n        return Promise.all(defs);\n    }\n    /**\n     * @param {String} value\n     * @returns {String}\n     */\n    setValue(value) {\n        this.odooEditor.resetContent(value);\n    }\n    /**\n     * Undo one step of change in the editor.\n     */\n    undo() {\n        this.odooEditor.historyUndo();\n    }\n    /**\n     * Redo one step of change in the editor.\n     */\n    redo() {\n        this.odooEditor.historyRedo();\n    }\n    /**\n     * Focus inside the editor.\n     *\n     * Set cursor to the editor latest position before blur or to the last editable node, ready to type.\n     */\n    focus() {\n        if (this.odooEditor && !this.odooEditor.historyResetLatestComputedSelection(true)) {\n            // If the editor don't have an history step to focus to,\n            // We place the cursor after the end of the editor exiting content.\n            const range = document.createRange();\n            const elementToTarget = this.$editable[0].lastElementChild ? this.$editable[0].lastElementChild : this.$editable[0];\n            range.selectNodeContents(elementToTarget);\n            range.collapse();\n\n            const selection = this.odooEditor.document.getSelection();\n            selection.removeAllRanges();\n            selection.addRange(range);\n        }\n    }\n    getDeepRange() {\n        return getDeepRange(this.odooEditor.editable);\n    }\n    closestElement(...args) {\n        return closestElement(...args);\n    }\n    async cleanForSave(editable = this.odooEditor.editable) {\n        if (this.odooEditor) {\n            this.odooEditor.cleanForSave(editable);\n            this._attachHistoryIds(editable);\n        }\n\n        if (this.state.showSnippetsMenu) {\n            const cleanedProms = [];\n            this.snippetsMenuBus.trigger(\"CLEAN_FOR_SAVE\", { proms: cleanedProms });\n            await Promise.all(cleanedProms);\n        }\n    }\n    isSelectionInEditable() {\n        return this.odooEditor.isSelectionInEditable();\n    }\n    /**\n     * Start or resume the Odoo field changes muation observers.\n     *\n     * Necessary to keep all copies of a given field at the same value throughout the page.\n     */\n    _observeOdooFieldChanges() {\n        const observerOptions = {\n            childList: true,\n            subtree: true,\n            attributes: true,\n            characterData: true,\n            attributeOldValue: true,\n        };\n        if (this.odooFieldObservers) {\n            for (let observerData of this.odooFieldObservers) {\n                observerData.observer.observe(observerData.field, observerOptions);\n            }\n        } else {\n            const odooFieldSelector = '[data-oe-model], [data-oe-translation-source-sha]';\n            const $odooFields = this.$editable.find(odooFieldSelector);\n            const renderingClassesSelector = this.odooEditor.options.renderingClasses\n                .map(className => `.${className}`).join(\", \");\n            this.odooFieldObservers = [];\n\n            $odooFields.each((i, field) => {\n                const observer = new MutationObserver((mutations) => {\n                    mutations = this.odooEditor.filterMutationRecords(mutations);\n                    mutations = mutations.filter(rec =>\n                        !(rec.type === \"attributes\" && (rec.attributeName.startsWith(\"data-oe-t\")))\n                    );\n                    if (!mutations.length) {\n                        return;\n                    }\n\n                    let $node = $(field);\n                    // Do not forward \"unstyled\" copies to other nodes.\n                    if ($node.hasClass('o_translation_without_style')) {\n                        return;\n                    }\n                    let $nodes = $odooFields.filter(function () {\n                        return this !== field;\n                    });\n                    if ($node.data('oe-model')) {\n                        $nodes = $nodes.filter('[data-oe-model=\"' + $node.data('oe-model') + '\"]')\n                            .filter('[data-oe-id=\"' + $node.data('oe-id') + '\"]')\n                            .filter('[data-oe-field=\"' + $node.data('oe-field') + '\"]');\n                    }\n\n                    if ($node.data('oe-translation-source-sha')) {\n                        $nodes = $nodes.filter('[data-oe-translation-source-sha=\"' + $node.data('oe-translation-source-sha') + '\"]');\n                    }\n                    if ($node.data('oe-type')) {\n                        $nodes = $nodes.filter('[data-oe-type=\"' + $node.data('oe-type') + '\"]');\n                    }\n                    if ($node.data('oe-expression')) {\n                        $nodes = $nodes.filter('[data-oe-expression=\"' + $node.data('oe-expression') + '\"]');\n                    } else if ($node.data('oe-xpath')) {\n                        $nodes = $nodes.filter('[data-oe-xpath=\"' + $node.data('oe-xpath') + '\"]');\n                    }\n                    if ($node.data('oe-contact-options')) {\n                        $nodes = $nodes.filter(\"[data-oe-contact-options='\" + $node[0].dataset.oeContactOptions + \"']\");\n                    }\n\n                    let nodes = $node.get();\n\n                    if ($node.data('oe-type') === \"many2one\") {\n                        $nodes = $nodes.add($('[data-oe-model]')\n                            .filter(function () {\n                                return this !== $node[0] && nodes.indexOf(this) === -1;\n                            })\n                            .filter('[data-oe-many2one-model=\"' + $node.data('oe-many2one-model') + '\"]')\n                            .filter('[data-oe-many2one-id=\"' + $node.data('oe-many2one-id') + '\"]')\n                            .filter('[data-oe-type=\"many2one\"]'));\n\n                        $nodes = $nodes.add($('[data-oe-model]')\n                            .filter(function () {\n                                return this !== $node[0] && nodes.indexOf(this) === -1;\n                            })\n                            .filter('[data-oe-model=\"' + $node.data('oe-many2one-model') + '\"]')\n                            .filter('[data-oe-id=\"' + $node.data('oe-many2one-id') + '\"]')\n                            .filter('[data-oe-field=\"name\"]'));\n                    }\n\n                    // TODO adapt in master: remove this and only use the\n                    //  `_pauseOdooFieldObservers(field)` call.\n                    this.__odooFieldObserversToPause = this.odooFieldObservers.filter(\n                        // Exclude inner translation fields observers. They\n                        // still handle translation synchronization inside the\n                        // targeted field.\n                        observerData => !observerData.field.dataset.oeTranslationSourceSha ||\n                            !field.contains(observerData.field)\n                    );\n                    this._pauseOdooFieldObservers();\n                    // Tag the date fields to only replace the value\n                    // with the original date value once (see mouseDown event)\n                    if ($node.hasClass('o_editable_date_field_format_changed')) {\n                        $nodes.addClass('o_editable_date_field_format_changed');\n                    }\n                    // Ignore the editor's rendering classes when copying field\n                    // content.\n                    const fieldNodeClone = $node[0].cloneNode(true);\n                    for (const node of fieldNodeClone.querySelectorAll(renderingClassesSelector)) {\n                        node.classList.remove(...this.odooEditor.options.renderingClasses);\n                    }\n                    const html = $(fieldNodeClone).html();\n                    this.odooEditor.withoutRollback(() => {\n                        for (const node of $nodes) {\n                            if (node.classList.contains('o_translation_without_style')) {\n                                // For generated elements such as the navigation\n                                // labels of website's table of content, only the\n                                // text of the referenced translation must be used.\n                                const text = $node.text();\n                                if (node.innerText !== text) {\n                                    node.innerText = text;\n                                }\n                                continue;\n                            }\n                            if (node.innerHTML !== html) {\n                                node.innerHTML = html;\n                            }\n                        }\n                    });\n                    this._observeOdooFieldChanges();\n                });\n                observer.observe(field, observerOptions);\n                this.odooFieldObservers.push({field: field, observer: observer});\n            });\n        }\n    }\n    /**\n     * Stop the field changes mutation observers.\n     */\n    _pauseOdooFieldObservers() {\n        // TODO adapt in master: remove this and directly exclude observers with\n        // targets inside the current field (we use `this.odooFieldObservers`\n        // as fallback for compatibility here).\n        const fieldObserversData = this.__odooFieldObserversToPause || this.odooFieldObservers;\n        for (let observerData of fieldObserversData) {\n            observerData.observer.disconnect();\n        }\n    }\n    /**\n     * Open the link tools or the image link tool depending on the selection.\n     */\n    openLinkToolsFromSelection() {\n        const targetEl = this.odooEditor.document.getSelection().getRangeAt(0).startContainer;\n        // Link tool is different if the selection is an image or a text.\n        if (targetEl.nodeType === Node.ELEMENT_NODE\n                && (targetEl.tagName === 'IMG' || targetEl.querySelectorAll('img').length === 1)) {\n            this.odooEditor.dispatchEvent(new Event('activate_image_link_tool'));\n            return;\n        }\n        this.toggleLinkTools();\n    }\n    /**\n     * Toggle the Link tools/dialog to edit links. If a snippet menu is present,\n     * use the link tools, otherwise use the dialog.\n     *\n     * @param {boolean} [options.forceOpen] default: false\n     * @param {boolean} [options.forceDialog] force to open the dialog\n     * @param {boolean} [options.link] The anchor element to edit if it is known.\n     * @param {boolean} [options.shoudFocusUrl=true] Disable the automatic focusing of the URL field.\n     */\n    async toggleLinkTools(options = {}) {\n        const shouldFocusUrl = options.shouldFocusUrl === undefined ? true : options.shouldFocusUrl;\n\n        const linkEl = getInSelection(this.odooEditor.document, 'a');\n        if (linkEl && (!linkEl.matches(this.customizableLinksSelector) || !linkEl.isContentEditable)) {\n            return;\n        }\n        if (this.state.showSnippetsMenu && !options.forceDialog) {\n            if (options.link && options.link.querySelector(mediaSelector) &&\n                    !options.link.textContent.trim() && wysiwygUtils.isImg(this.lastElement)) {\n                // If the link contains a media without text, the link is\n                // editable in the media options instead.\n                if (options.shoudFocusUrl) {\n                    // Wait for the editor panel to be fully updated.\n                    this.mutex.exec(() => {\n                        this.odooEditor.dispatchEvent(new Event('activate_image_link_tool'));\n                    });\n                }\n                return;\n            }\n            if (options.forceOpen || !this.state.linkToolProps) {\n                const $button = $(this.toolbarEl.querySelector('#create-link'));\n                if (!this.state.linkToolProps || ![options.link, ...wysiwygUtils.ancestors(options.link)].includes(this.linkToolsInfos.link)) {\n                    const { link } = getOrCreateLink({\n                        containerNode: this.odooEditor.editable,\n                        startNode: options.link || this.lastMediaClicked,\n                    });\n                    if (!link) {\n                        return;\n                    }\n                    const addHintClasses = () => {\n                        this.odooEditor.observerUnactive(\"hint_classes\");\n                        link.classList.add('oe_edited_link');\n                        $button.addClass('active');\n                        this.odooEditor.observerActive(\"hint_classes\");\n                    };\n                    const removeHintClasses = () => {\n                        this.odooEditor.observerUnactive(\"hint_classes\");\n                        link.classList.remove('oe_edited_link');\n                        $button.removeClass('active');\n                        this.odooEditor.observerActive(\"hint_classes\");\n                    };\n                    this.linkToolsInfos = {\n                        onDestroy: () => {},\n                        link,\n                        removeHintClasses,\n                    };\n\n                    addHintClasses();\n                    this.state.linkToolProps = {\n                        ...this.options.linkOptions,\n                        wysiwyg: this,\n                        editable: this.odooEditor.editable,\n                        link,\n                        // If the link contains an image or an icon do not\n                        // display the label input (e.g. some mega menu links).\n                        needLabel: !link.querySelector('.fa, img'),\n                        shouldFocusUrl,\n                        $button,\n                        onColorCombinationClassChange: (colorCombinationClass) => {\n                            this.linkToolsInfos.colorCombinationClass = colorCombinationClass;\n                        },\n                        onPreApplyLink: removeHintClasses,\n                        onPostApplyLink: addHintClasses,\n                        onDestroy: () => {\n                            removeHintClasses();\n                            this.linkToolsInfos.onDestroy();\n                        },\n                        getColorpickerTemplate: this.getColorpickerTemplate.bind(this),\n                    };\n                }\n                // update the shouldFocusUrl prop to focus on url when double click and click edit link\n                this.state.linkToolProps.shouldFocusUrl = shouldFocusUrl;\n                this.odooEditor.document.removeEventListener('click', this._onClick, true);\n                document.removeEventListener('click', this._onClick, true);\n                this._onClick = ev => {\n                    if (\n                        !ev.target.closest('#create-link') &&\n                        !ev.target.closest(\".o_technical_modal\") &&\n                        !ev.target.closest('.oe-toolbar') &&\n                        !ev.target.closest('.ui-autocomplete') &&\n                        (!this.state.linkToolProps || ![ev.target, ...wysiwygUtils.ancestors(ev.target)].includes(this.linkToolsInfos.link))\n                    ) {\n                        // Destroy the link tools on click anywhere outside the\n                        // toolbar if the target is the orgiginal target not in the original target.\n                        this.destroyLinkTools();\n                        this.odooEditor.document.removeEventListener('click', this._onClick, true);\n                        document.removeEventListener('click', this._onClick, true);\n                    }\n                };\n                this.odooEditor.document.addEventListener('click', this._onClick, true);\n                document.addEventListener('click', this._onClick, true);\n            } else {\n                this.destroyLinkTools();\n            }\n        } else {\n            const historyStepIndex = this.odooEditor.historySize() - 1;\n            this.odooEditor.historyPauseSteps();\n            let { link } = getOrCreateLink({\n                containerNode: this.odooEditor.editable,\n                startNode: options.link,\n            });\n            if (!link) {\n                this.odooEditor.historyUnpauseSteps();\n                return\n            }\n            this._shouldDelayBlur = true;\n            this.env.services.dialog.add(LinkDialog, {\n                ...this.options.linkOptions,\n                editable: this.odooEditor.editable,\n                link,\n                needLabel: true && !link.querySelector('img'),\n                focusField: link.innerHTML ? 'url' : '',\n                onSave: (data) => {\n                    if (!data) {\n                        return;\n                    }\n                    getDeepRange(this.$editable[0], {range: data.range, select: true});\n                    if (this.options.userGeneratedContent) {\n                        data.rel = 'ugc';\n                    }\n                    data.linkDialog.applyLinkToDom(data);\n                    this.odooEditor.historyUnpauseSteps();\n                    this.odooEditor.historyStep();\n                    const link = data.linkDialog.$link[0];\n                    const linkIndex = childNodeIndex(link);\n                    setSelection(link.parentElement, linkIndex+1, link.parentElement, linkIndex+1, false);\n                },\n                onClose: () => {\n                    this.odooEditor.historyUnpauseSteps();\n                    this.odooEditor.historyRevertUntil(historyStepIndex)\n                }\n            });\n        }\n    }\n    /**\n     * Open one of the ChatGPTDialogs to generate or modify content.\n     *\n     * @param {'prompt'|'alternatives'|'translate'} [mode='prompt']\n     * @param {object} options\n     * @param {String} [options.language]\n     */\n    openChatGPTDialog(mode = 'prompt', options={}) {\n        const restore = preserveCursor(this.odooEditor.document);\n        const params = {\n            insert: content => {\n                this.odooEditor.historyPauseSteps();\n                const insertedNodes = this.odooEditor.execCommand('insert', content);\n                this.odooEditor.historyUnpauseSteps();\n                this.notification.add(_t('Your content was successfully generated.'), {\n                    title: _t('Content generated'),\n                    type: 'success',\n                });\n                this.odooEditor.historyStep();\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 = insertedNodes?.length && closestElement(insertedNodes[insertedNodes.length - 1]);\n                if (start && end) {\n                    const divContainer = this.odooEditor.editable.parentElement;\n                    let [parent, left, top] = [start.offsetParent, start.offsetLeft, start.offsetTop - start.scrollTop];\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 = `${Math.max(start.offsetWidth, end.offsetWidth) + (FRAME_PADDING * 2)}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        };\n        if (mode === 'alternatives' || mode === 'translate') {\n            params.originalText = this.odooEditor.document.getSelection().toString() || '';\n        }\n        if (mode === 'translate') {\n            params.language = options.language;\n        }\n        this.odooEditor.document.getSelection().collapseToEnd();\n        this.env.services.dialog.add(\n            mode === 'prompt' ? ChatGPTPromptDialog : mode === 'translate' ? ChatGPTTranslateDialog : ChatGPTAlternativesDialog,\n            params,\n            { onClose: restore },\n        );\n    }\n    /**\n     * Removes the current Link.\n     */\n    removeLink() {\n        if (this.state.showSnippetsMenu && wysiwygUtils.isImg(this.lastElement)) {\n            this.mutex.exec(() => {\n                this.odooEditor.dispatchEvent(new Event('deactivate_image_link_tool'));\n            });\n        } else {\n            this.odooEditor.execCommand('unlink');\n        }\n    }\n    /**\n     * Destroy the Link tools/dialog and restore the selection.\n     */\n    // todo: review me\n    async destroyLinkTools() {\n        if (this.state.linkToolProps) {\n            const selection = this.odooEditor.document.getSelection();\n            const link = this.linkToolsInfos.link;\n            let anchorNode\n            let focusNode;\n            let anchorOffset = 0;\n            let focusOffset;\n            if (selection && link.parentElement) {\n                // Focus the link after the dialog element is removed.\n                if (shouldUnlink(this.linkToolsInfos.link, this.linkToolsInfos.colorCombinationClass)) {\n                    if (link.childNodes.length) {\n                        anchorNode = link.childNodes[0];\n                        focusNode = link.childNodes[link.childNodes.length - 1];\n                    } else {\n                        const parent = link.parentElement;\n                        const index = Array.from(parent.childNodes).indexOf(link);\n                        anchorNode = focusNode = parent;\n                        anchorOffset = focusOffset = index;\n                    }\n                } else {\n                    const commonBlock = selection.rangeCount && closestBlock(selection.getRangeAt(0).commonAncestorContainer);\n                    if (commonBlock && link.contains(commonBlock)) {\n                        [anchorNode, focusNode] = [commonBlock, commonBlock];\n                    } else if (!this.$editable[0].contains(selection.anchorNode)) {\n                        [anchorNode, focusNode] = [link, link];\n                    }\n                }\n                if (!focusOffset && focusNode) {\n                    focusOffset = focusNode.childNodes.length || focusNode.length;\n                }\n            }\n            this.linkToolsInfos.removeHintClasses();\n            if (anchorNode) {\n                setSelection(anchorNode, anchorOffset, focusNode, focusOffset, false);\n            }\n            this.state.linkToolProps = undefined;\n        }\n    }\n    /**\n     * Take an image's URL and display it in a fullscreen viewer.\n     *\n     * @todo should use `useFileViewer` instead once Wysiwyg becomes an Owl Component.\n     * @param {string} url\n     */\n    showImageFullscreen(url) {\n        const viewerId = `web.file_viewer${fileViewerId++}`;\n        registry.category(\"main_components\").add(viewerId, {\n            Component: FileViewer,\n            props: {\n                files: [{\n                        isImage: true,\n                        isViewable: true,\n                        displayName: url,\n                        defaultSource: url,\n                        downloadUrl: url,\n                }],\n                startIndex: 0,\n                close: () => {\n                    registry.category('main_components').remove(viewerId);\n                },\n            },\n        });\n        this.odooEditor.document.getSelection()?.collapseToEnd();\n        this.odooEditor.editable.blur();\n    }\n    /**\n     * Open the media dialog.\n     *\n     * Used to insert or change image, icon, document and video.\n     *\n     * @param {object} params\n     * @param {Node} [params.node] Optionnal\n     * @param {Node} [params.htmlClass] Optionnal\n     * @param {Class} [params.MediaDialog] Optional\n     */\n    openMediaDialog(params = {}) {\n        const sel = this.odooEditor.document.getSelection();\n\n        if (!sel.rangeCount) {\n            return;\n        }\n        const range = sel.getRangeAt(0);\n        // We lose the current selection inside the content editable when we\n        // click the media dialog button so we need to be able to restore the\n        // selection when the modal is closed.\n        const restoreSelection = preserveCursor(this.odooEditor.document);\n\n        const editable = OdooEditorLib.closestElement(params.node || range.startContainer, '.o_editable') || this.odooEditor.editable;\n        const { resModel, resId, field, type } = this._getRecordInfo(editable);\n\n        this.env.services.dialog.add(params.MediaDialog || MediaDialog, {\n            resModel,\n            resId,\n            useMediaLibrary: !!(field && (resModel === 'ir.ui.view' && field === 'arch' || type === 'html')),\n            media: params.node,\n            save: this._onMediaDialogSave.bind(this, {\n                node: params.node,\n                restoreSelection: restoreSelection,\n            }),\n            onAttachmentChange: this._onAttachmentChange.bind(this),\n            close: () => restoreSelection(),\n            ...this.options.mediaModalParams,\n            ...params,\n            noVideos: !this.options.allowCommandVideo,\n        });\n    }\n    // todo: test me\n    showEmojiPicker() {\n        const targetEl = this.odooEditor.document.getSelection();\n        const closest = closestBlock(targetEl.anchorNode);\n        const restoreSelection = preserveCursor(this.odooEditor.document);\n\n        this.popover.add(closest, EmojiPicker, {\n                onSelect: (str) => {\n                    restoreSelection();\n                    this.odooEditor.execCommand('insert', str);\n                },\n            }, {\n                onPositioned: (popover) => {\n                    restoreSelection();\n                    const rangePosition = getRangePosition(popover, this.options.document, {\n                        getContextFromParentRect: this.options.getContextFromParentRect,\n                    });\n                    popover.style.top = rangePosition.top + 'px';\n                    popover.style.left = rangePosition.left + 'px';\n                    const oInputBox = popover.querySelector('input');\n                    oInputBox?.focus();\n                },\n            },\n        );\n    }\n    /**\n     * Sets custom CSS Variables on the snippet menu element.\n     * Used for color previews and color palette to get the color\n     * values of the editable. (e.g. if the editable is in an iframe\n     * with different SCSS color values as the top window.)\n     *\n     * @param {HTMLElement} element\n     */\n    setCSSVariables(element) {\n        const stylesToCopy = weUtils.EDITOR_COLOR_CSS_VARIABLES;\n\n        for (const style of stylesToCopy) {\n            let value = weUtils.getCSSVariableValue(style);\n            if (value.startsWith(\"'\") && value.endsWith(\"'\")) {\n                // Gradient values are recovered within a string.\n                value = value.substring(1, value.length - 1);\n            }\n            element.style.setProperty(`--we-cp-${style}`, value);\n        }\n\n        element.classList.toggle('o_we_has_btn_outline_primary',\n            weUtils.getCSSVariableValue('btn-primary-outline') === 'true');\n        element.classList.toggle('o_we_has_btn_outline_secondary',\n            weUtils.getCSSVariableValue('btn-secondary-outline') === 'true');\n    }\n    /**\n     * Detached function to allow overriding.\n     *\n     * @param {Object} params binded @see openMediaDialog\n     * @param {Element} element provided by the dialog\n     */\n    _onMediaDialogSave(params, element) {\n        params.restoreSelection();\n        if (!element) {\n            return;\n        }\n\n        if (params.node) {\n            const changedIcon = isIconElement(params.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                    params.node.setAttribute(attribute.nodeName, attribute.nodeValue);\n                }\n            } else {\n                params.node.replaceWith(element);\n            }\n            this.odooEditor.unbreakableStepUnactive();\n\n            if (params.node.matches(\".oe_unremovable\")) {\n                // The \"oe_unremovable\" class prevents element deletion and must\n                // be removed during the \"historyStep\" to allow media\n                // replacement. If the class remains, the \"sanitize\" function in\n                // \"historyStep\" will block the replacement.\n                params.node.classList.remove(\"oe_unremovable\");\n                element.classList.remove(\"oe_unremovable\");\n                this.odooEditor.historyStep();\n                this.odooEditor.observerUnactive(\"unremovable\");\n                element.classList.add(\"oe_unremovable\");\n                this.odooEditor.observerActive(\"unremovable\");\n            } else {\n                this.odooEditor.historyStep();\n            }\n\n            // Refocus again to save updates when calling `_onWysiwygBlur`\n            this.odooEditor.editable.focus();\n        } else {\n            const result = this.odooEditor.execCommand('insert', element);\n            // Refocus again to save updates when calling `_onWysiwygBlur`\n            this.odooEditor.editable.focus();\n            return result;\n        }\n\n        if (this.state.showSnippetsMenu) {\n            this.snippetsMenuBus.trigger(\"ACTIVATE_SNIPPET\", {\n                $snippet: $(element),\n                onSuccess: () => {\n                    if (element.tagName === 'IMG') {\n                        $(element).trigger('image_changed');\n                    }\n                }\n            });\n        }\n    }\n    getInSelection(selector) {\n        return getInSelection(this.odooEditor.document, selector);\n    }\n    /**\n     * Adds an empty action in the mutex. Can be used to wait for some options\n     * to be initialized before doing something else.\n     *\n     * @returns {Promise}\n     */\n    waitForEmptyMutexAction() {\n        if (this.state.showSnippetsMenu) {\n            return this.mutex.exec(() => null);\n        }\n        return Promise.resolve();\n    }\n    getColorpickerTemplate() {\n        // Public user using the editor may have a colorpalette but with\n        // the default wysiwyg ones.\n        if (!session.is_website_user) {\n            return this.getColorPickerTemplateService();\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    _getRecordInfo() {\n        const { res_model: resModel, res_id: resId } = this.options.recordInfo;\n        return { resModel, resId };\n    }\n    /**\n     * Returns an instance of the snippets menu.\n     *\n     * @returns {Promise<Component>}\n     */\n    async getSnippetsMenuClass() {\n        const snippetsEditor = await odoo.loader.modules.get('@web_editor/js/editor/snippets.editor')[Symbol.for('default')];\n        const { SnippetsMenu } = snippetsEditor;\n        return SnippetsMenu;\n    }\n    get snippetsMenuOptions() {\n        return {\n            ...this.options,\n            wysiwyg: this,\n            selectorEditableArea: '.o_editable',\n            mutex: this.mutex,\n        };\n    }\n    _setToolbarProps() {\n        this.state.toolbarProps = {\n            ...this.options.toolbarOptions,\n            onColorpaletteDropdownShow: this.onColorpaletteDropdownShow.bind(this),\n            onColorpaletteDropdownHide: this.onColorpaletteDropdownHide.bind(this),\n            textColorPaletteProps: this.colorPalettesProps.text,\n            backgroundColorPaletteProps: this.colorPalettesProps.background,\n            showRemoveFormat: this.state.snippetsMenuFolded || !this.options.snippets,\n        };\n    }\n    _configureToolbar(options) {\n        const $toolbar = $(this.toolbarEl);\n        // Prevent selection loss when interacting with the toolbar buttons.\n        $toolbar.find('.btn-group').on('mousedown', e => {\n            if (\n                // Prevent when clicking on btn-group but not on dropdown items.\n                !e.target.closest('.dropdown-menu') ||\n                // Unless they have a data-call in which case there is an editor\n                // command that is bound to it so we need to preventDefault.\n                e.target.closest('.btn') && e.target.closest('.btn').getAttribute('data-call')\n            ) {\n                e.preventDefault();\n            }\n        });\n        const openTools = e => {\n            e.preventDefault();\n            e.stopImmediatePropagation();\n            e.stopPropagation();\n            switch (e.currentTarget.id) {\n                case 'create-link':\n                    this.toggleLinkTools();\n                    break;\n                case 'media-insert':\n                case 'media-replace':\n                    this.openMediaDialog({ node: this.lastMediaClicked });\n                    break;\n                case 'media-description': {\n                    const allEscQuots = /&quot;/g;\n                    const alt = ($(this.lastMediaClicked).attr('alt') || \"\").replace(allEscQuots, '\"');\n                    const tag_title = (\n                        $(this.lastMediaClicked).attr('title') ||\n                        $(this.lastMediaClicked).data('original-title') ||\n                        \"\"\n                    ).replace(allEscQuots, '\"');\n\n                    this.env.services.dialog.add(AltDialog, {\n                        alt,\n                        tag_title,\n                        confirm: (newAlt, newTitle) => {\n                            if (newAlt) {\n                                this.lastMediaClicked.setAttribute('alt', newAlt);\n                            } else {\n                                this.lastMediaClicked.removeAttribute('alt');\n                            }\n                            if (newTitle) {\n                                this.lastMediaClicked.setAttribute('title', newTitle);\n                            } else {\n                                this.lastMediaClicked.removeAttribute('title');\n                            }\n                        },\n                    });\n                    break;\n                }\n                case 'open-chatgpt': {\n                    this.openChatGPTDialog(this.odooEditor.document.getSelection()?.isCollapsed ? 'prompt' : 'alternatives');\n                    break;\n                }\n            }\n        };\n        if (!options.snippets) {\n            $toolbar.find('#justify, #media-insert').remove();\n        }\n        $toolbar.find('#image-fullscreen').click(() => {\n            if (!this.lastMediaClicked?.src) {\n                return;\n            }\n            this.showImageFullscreen(this.lastMediaClicked.src);\n        });\n        $toolbar.find('#media-insert, #media-replace, #media-description').click(openTools);\n        $toolbar.find('#create-link').click(openTools);\n        $toolbar.find('#open-chatgpt').click(openTools);\n        $toolbar.on('click', '#translate .lang', (e) => {\n            e.preventDefault();\n            e.stopImmediatePropagation();\n            e.stopPropagation();\n            const language = e.target.dataset.value;\n            this.openChatGPTDialog('translate', { language });\n        });\n        $toolbar.find('#image-shape div, #fa-spin').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            this.lastMediaClicked.classList.toggle(e.target.id);\n            e.target.classList.toggle('active', $(this.lastMediaClicked).hasClass(e.target.id));\n        });\n        const $imageWidthButtons = $toolbar.find('#image-width div');\n        $imageWidthButtons.click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            this.lastMediaClicked.style.width = e.target.id;\n            for (const button of $imageWidthButtons) {\n                button.classList.toggle('active', this.lastMediaClicked.style.width === button.id);\n            }\n        });\n        $toolbar.find('#image-padding .dropdown-item').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            $(this.lastMediaClicked).removeClass((index, className) => (\n                (className.match(/(^|\\s)padding-\\w+/g) || []).join(' ')\n            )).addClass(e.target.dataset.class);\n        });\n        $toolbar.on('mousedown', e => {\n            const justifyBtn = e.target.closest('#justify div.btn');\n            if (!justifyBtn || !this.lastMediaClicked) {\n                return;\n            }\n            e.originalEvent.stopImmediatePropagation();\n            e.originalEvent.stopPropagation();\n            e.originalEvent.preventDefault();\n            const mode = justifyBtn.id.replace('justify', '').toLowerCase();\n            const classes = mode === 'center' ? ['d-block', 'mx-auto'] : ['float-' + mode];\n            const doAdd = classes.some(className => !this.lastMediaClicked.classList.contains(className));\n            this.lastMediaClicked.classList.remove('float-start', 'float-end');\n            if (this.lastMediaClicked.classList.contains('mx-auto')) {\n                this.lastMediaClicked.classList.remove('d-block', 'mx-auto');\n            }\n            if (doAdd) {\n                this.lastMediaClicked.classList.add(...classes);\n            }\n            this._updateMediaJustifyButton(justifyBtn.id);\n        });\n        $toolbar.find('#image-crop').click(() => this._showImageCrop());\n        $toolbar.find('#image-transform').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            const $image = $(this.lastMediaClicked);\n            const imgTransformBtn = this.toolbarEl.querySelector('#image-transform');\n            if ($image.data('transfo-destroy')) {\n                $image.removeData('transfo-destroy');\n                return;\n            }\n            $image.transfo({document: this.odooEditor.document});\n            const destroyTransfo = () => {\n                $image.transfo('destroy');\n                $(this.odooEditor.document).off('mousedown', mousedown).off('mouseup', mouseup);\n                this.odooEditor.document.removeEventListener('keydown', keydown);\n            }\n            const mouseup = () => {\n                imgTransformBtn.classList.toggle('active', $image[0].matches('[style*=\"transform\"]'));\n            };\n            $(this.odooEditor.document).on('mouseup', mouseup);\n            const mousedown = mousedownEvent => {\n                if (!$(mousedownEvent.target).closest('.transfo-container').length) {\n                    destroyTransfo();\n                }\n                if ($(mousedownEvent.target).closest('#image-transform').length) {\n                    $image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(/[^;]*transform[\\w:]*;?/g, ''));\n                    imgTransformBtn.classList.remove('active');\n                }\n                $image.trigger('content_changed');\n            };\n            $(this.odooEditor.document).on('mousedown', mousedown);\n            const keydown = keydownEvent => {\n                if (keydownEvent.key === 'Escape') {\n                    keydownEvent.stopImmediatePropagation();\n                    destroyTransfo();\n                }\n            };\n            this.odooEditor.document.addEventListener('keydown', keydown);\n        });\n        $toolbar.find('#image-delete').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            $(this.lastMediaClicked).remove();\n            this.lastMediaClicked = undefined;\n            this.odooEditor.toolbarHide();\n        });\n        $toolbar.find('#fa-resize div').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            const $target = $(this.lastMediaClicked);\n            const sValue = e.target.dataset.value;\n            $target.attr('class', $target.attr('class').replace(/\\s*fa-[0-9]+x/g, ''));\n            if (+sValue > 1) {\n                $target.addClass('fa-' + sValue + 'x');\n            }\n            this._updateFaResizeButtons();\n        });\n        if (!options.snippets) {\n            // Scroll event does not bubble.\n            document.addEventListener('scroll', this._onScroll, true);\n        }\n    }\n\n    _showImageCrop() {\n        if (!this.lastMediaClicked) {\n            return;\n        }\n        this.imageCropProps.media = this.lastMediaClicked;\n        this.imageCropProps.showCount++;\n        this.odooEditor.toolbarHide();\n        $(this.lastMediaClicked).on('image_cropper_destroyed', () => this.odooEditor.toolbarShow());\n    }\n    /**\n     * @private\n     * @param {jQuery} $\n     * @param {String} colorType 'text' or 'background'\n     * @returns {String} color\n     */\n    _getSelectedColor($, colorType) {\n        const selection = this.odooEditor.document.getSelection();\n        if (!selection) return;\n        const range = selection.rangeCount && selection.getRangeAt(0);\n        const targetNode = range && range.startContainer;\n        const targetElement = targetNode && targetNode.nodeType === Node.ELEMENT_NODE\n            ? targetNode\n            : targetNode && targetNode.parentNode;\n        const backgroundImage = $(targetElement).css('background-image');\n        let backgroundGradient = false;\n        if (weUtils.isColorGradient(backgroundImage)) {\n            const textGradient = targetElement.classList.contains('text-gradient');\n            if (colorType === \"text\" && textGradient || colorType !== \"text\" && !textGradient) {\n                backgroundGradient = backgroundImage;\n            }\n        }\n        return backgroundGradient || $(targetElement).css(colorType === \"text\" ? 'color' : 'backgroundColor');\n    }\n    onColorpaletteDropdownHide(ev) {\n        return !(ev.clickEvent && ev.clickEvent.__isColorpickerClick);\n    }\n    onColorpaletteDropdownShow(colorType) {\n        const selectedColor = this._getSelectedColor($, colorType);\n        this.colorPalettesProps[colorType].resetTabCount++;\n        this.colorPalettesProps[colorType].selectedColor = selectedColor;\n\n        const selection = this.odooEditor.document.getSelection();\n        const range = selection.rangeCount && selection.getRangeAt(0);\n        this.hadNonCollapsedSelectionBeforeColorpicker = range && !selection.isCollapsed;\n\n        // The color_leave event will revert the mutations with\n        // `historyRevertCurrentStep`. We must stash the current\n        // mutations to prevent them from being reverted.\n        this.odooEditor.historyStash();\n    }\n    getColorPaletteTabChangeHandler(colorType) {\n        return (selectedTab) => {\n            this.colorPalettesProps[colorType].selectedTab = selectedTab;\n        }\n    }\n    _processAndApplyColor(colorType, color, previewMode) {\n        if (color && !isCSSColor(color) && !weUtils.isColorGradient(color)) {\n            color = (colorType === \"text\" ? 'text-' : 'bg-') + color;\n        }\n        const selectedTds = this.odooEditor.document.querySelectorAll('td.o_selected_td');\n        const applyTransparency =\n            color.startsWith('#') && // Check for hex color.\n            !selectedTds.length && // Do not apply to table cells.\n            colorType === 'background' && // Only apply on bg color.\n            // Check if color is coming from theme-colors tab.\n            this.colorPalettesProps.background.selectedTab === 'theme-colors';\n        // Apply default transparency to the selected common color to make\n        // text highlighting more usable between light and dark modes.\n        if (applyTransparency) {\n            const HEX_OPACITY = '99';\n            color = color.concat(HEX_OPACITY);\n        }\n        let coloredElements = this.odooEditor.execCommand('applyColor', color, colorType === 'text' ? 'color' : 'backgroundColor', this.lastMediaClicked);\n        // Some nodes returned by applyColor can be removed of the document by the sanitization in historyStep\n        coloredElements = coloredElements.filter(element => this.odooEditor.document.contains(element));\n\n        const coloredTds = coloredElements && coloredElements.length && Array.isArray(coloredElements) && coloredElements.filter(coloredElement => coloredElement.classList.contains('o_selected_td'));\n        if (coloredTds.length) {\n            const propName = colorType === 'text' ? 'color' : 'background-color';\n            for (const td of coloredTds) {\n                // Make it important so it has priority over selection color.\n                td.style.setProperty(propName, td.style[propName], previewMode ? 'important' : '');\n            }\n        } else if (color && !this.lastMediaClicked && coloredElements && coloredElements.length && Array.isArray(coloredElements)) {\n            // Ensure the selection in the fonts tags, otherwise an undetermined\n            // race condition could generate a wrong selection later.\n            const first = coloredElements[0];\n            const last = coloredElements[coloredElements.length - 1];\n\n            const sel = this.odooEditor.document.getSelection();\n            const range = sel.getRangeAt(0);\n            const isSelForward = sel.anchorNode === range.startContainer && sel.anchorOffset === range.startOffset;\n            range.setStart(first, 0);\n            range.setEnd(...endPos(last));\n                const { startContainer, startOffset, endContainer, endOffset } = getDeepRange(this.odooEditor.editable, { range });\n            if (isSelForward) {\n                sel.setBaseAndExtent(startContainer, startOffset, endContainer, endOffset);\n            } else {\n                sel.setBaseAndExtent(endContainer, endOffset, startContainer, startOffset);\n            }\n        }\n\n        const hexColor = this._colorToHex(color);\n        this.odooEditor.updateColorpickerLabels({\n            [colorType === 'text' ? 'text' : 'hiliteColor']: hexColor,\n        });\n    }\n    _colorToHex(color) {\n        if (color.startsWith('#')) {\n            return color;\n        } else if (weUtils.isColorGradient(color)) {\n            // return gradient the way it is: updateColorpickerLabels will handle it\n            return color;\n        } else {\n            let rgbColor;\n            if (color.startsWith('rgb')) {\n                rgbColor = color;\n            } else {\n                const $font = $(`<font class=\"${color}\"/>`);\n                $(document.body).append($font);\n                const propertyName = color.startsWith('text') ? 'color' : 'backgroundColor';\n                rgbColor = $font.css(propertyName);\n                $font.remove();\n            }\n            return rgbToHex(rgbColor);\n        }\n    }\n    /**\n     * Handle custom keyboard shortcuts.\n     */\n    _handleShortcuts(e) {\n        // Open the link tool when CTRL+K is pressed.\n        if (this.options.bindLinkTool && e && e.key === 'k' && (e.ctrlKey || e.metaKey)) {\n            e.preventDefault();\n            this.openLinkToolsFromSelection();\n        }\n        // Override selectAll (CTRL+A) to restrict it to the editable zone / current snippet and prevent traceback.\n        if (e && e.key === 'a' && (e.ctrlKey || e.metaKey)) {\n            e.preventDefault();\n            const selection = this.odooEditor.document.getSelection();\n            const containerSelector = '#wrap>*, .oe_structure>*, [contenteditable]';\n            const container =\n                (selection &&\n                    closestElement(selection.anchorNode, containerSelector)) ||\n                // In case a suitable container could not be found then the\n                // selection is restricted inside the editable area.\n                this.$editable.find(containerSelector)[0];\n            if (container) {\n                const range = document.createRange();\n                range.selectNodeContents(container);\n                selection.removeAllRanges();\n                selection.addRange(range);\n            }\n        }\n    }\n    /**\n     * Update any editor UI that is not handled by the editor itself.\n     */\n    _updateEditorUI(e) {\n        let selection = this.odooEditor.document.getSelection();\n        if (!selection) return;\n        const anchorNode = selection.anchorNode;\n        if (isProtected(anchorNode)) {\n            return;\n        }\n        if (this.odooEditor.document.querySelector(\".transfo-container\")) {\n            return;\n        }\n\n        this.odooEditor.automaticStepSkipStack();\n        // We need to use the editor's window so the tooltip displays in its\n        // document even if it's in an iframe.\n        const editorWindow = this.odooEditor.document.defaultView;\n        const $target = e ? editorWindow.$(e.target) : editorWindow.$();\n        // Restore paragraph dropdown button's default ID.\n        this.toolbarEl.querySelector('#mediaParagraphDropdownButton')?.setAttribute('id', 'paragraphDropdownButton');\n        // Only show the media tools in the toolbar if the current selected\n        // snippet is a media.\n        const isInMedia = $target.is(mediaSelector) && !$target.parent().hasClass('o_stars') && e.target &&\n            (e.target.isContentEditable || (e.target.parentElement && e.target.parentElement.isContentEditable));\n        this.toolbarEl.classList.toggle('oe-media', isInMedia);\n\n        for (const el of this.toolbarEl.querySelectorAll([\n            '#image-preview',\n            '#image-shape',\n            '#image-width',\n            '#image-padding',\n            '#image-edit',\n            '#media-replace',\n            ].join(','))) {\n            el.classList.toggle('d-none', !isInMedia);\n        }\n        // The image replace button is in the image options when the sidebar\n        // exists.\n        if (this.state.showSnippetsMenu && !this.state.snippetsMenuFolded && $target.is('img')) {\n            this.toolbarEl.querySelector('#media-replace')?.classList.toggle('d-none', true);\n        }\n        // Only show the image-transform, image-crop and media-description\n        // buttons if the current selected snippet is an image.\n        for (const el of this.toolbarEl.querySelectorAll([\n            '#image-transform',\n            '#image-crop',\n            '#media-description',\n            ].join(','))) {\n            el.classList.toggle('d-none', !isInMedia || !$target.is('img'));\n        }\n        this.lastMediaClicked = isInMedia && e.target;\n        this.lastElement = $target[0];\n        // Hide the irrelevant text buttons for media.\n        for (const el of this.toolbarEl.querySelectorAll([\n            '#style',\n            '#decoration',\n            '#font-size',\n            '#justifyFull',\n            '#list',\n            '#colorInputButtonGroup',\n            '#media-insert', // \"Insert media\" should be replaced with \"Replace media\".\n            '#chatgpt', // Chatgpt should be removed when media is in selection.\n            '#translate' // Translate button should be removed when media is in selection.\n        ].join(','))){\n            el.classList.toggle('d-none', isInMedia);\n        }\n        // Some icons are relevant for icons, that aren't for other media.\n        for (const el of this.toolbarEl.querySelectorAll('#colorInputButtonGroup')) {\n            el.classList.toggle('d-none', isInMedia && !$target.is('.fa'));\n        }\n        for (const el of this.toolbarEl.querySelectorAll('.only_fa')) {\n            el.classList.toggle('d-none', !isInMedia || !$target.is('.fa'));\n        }\n        // Hide unsuitable buttons for icon\n        if ($target.is('.fa')) {\n            for (const el of this.toolbarEl.querySelectorAll([\n                '#image-shape',\n                '#image-width',\n                '#image-edit',\n            ].join(','))) {\n                el.classList.toggle('d-none', true);\n            }\n        }\n        // Unselect all media.\n        this.$editable.find('.o_we_selected_image').removeClass('o_we_selected_image');\n        if (isInMedia) {\n            this.odooEditor.automaticStepSkipStack();\n            // Select the media in the DOM.\n            const selection = this.odooEditor.document.getSelection();\n            const range = this.odooEditor.document.createRange();\n            range.selectNode(this.lastMediaClicked);\n            selection.removeAllRanges();\n            selection.addRange(range);\n            // Toggle the 'active' class on the active image tool buttons.\n            for (const button of this.toolbarEl.querySelectorAll('#image-shape div, #fa-spin')) {\n                button.classList.toggle('active', $(e.target).hasClass(button.id));\n            }\n            for (const button of this.toolbarEl.querySelectorAll('#image-width div')) {\n                button.classList.toggle('active', e.target.style.width === button.id);\n            }\n            this.toolbarEl.querySelector('#image-transform').classList.toggle('active', e.target.matches('[style*=\"transform\"]'));\n            this._updateMediaJustifyButton();\n            this._updateFaResizeButtons();\n        }\n        if (isInMedia && !this.options.onDblClickEditableMedia) {\n            // Handle the media/link's tooltip.\n            this.showTooltip = true;\n            this.tooltipTimeouts.push(setTimeout(() => {\n                // Do not show tooltip on double-click and if there is already one\n                if (!this.showTooltip || $target.attr('title') !== undefined) {\n                    return;\n                }\n                // Tooltips need to be cleared before leaving the editor.\n                this.saving_mutex.exec(() => {\n                    const removeTooltip = this.popover.add(e.target, Tooltip, { tooltip: _t('Double-click to edit') });\n                    this.tooltipTimeouts.push(setTimeout(() => removeTooltip(), 800));\n                });\n            }, 400));\n        }\n        // Toolbar might have changed size, update its position.\n        this.odooEditor.updateToolbarPosition();\n        // Update color of already opened colorpickers.\n        setTimeout(() => {\n            for (const colorType in this.colorPalettesProps) {\n                const selectedColor = this._getSelectedColor($, colorType);\n                if (selectedColor) {\n                    // If the palette was already opened (e.g. modifying a gradient), the new DOM state\n                    // must be reflected in the palette, but the tab selection must not be impacted.\n                    this.colorPalettesProps[colorType].selectedColor = selectedColor;\n                }\n            }\n        });\n    }\n    _updateMediaJustifyButton(commandState) {\n        if (!this.lastMediaClicked) {\n            return;\n        }\n        const $paragraphDropdownButton = $(this.toolbarEl).find('#paragraphDropdownButton, #mediaParagraphDropdownButton');\n        // Change the ID to prevent OdooEditor from controlling it as this is\n        // custom behavior for media.\n        $paragraphDropdownButton.attr('id', 'mediaParagraphDropdownButton');\n        let resetAlignment = true;\n        if (!commandState) {\n            const justifyMapping = [\n                ['float-start', 'justifyLeft'],\n                ['mx-auto', 'justifyCenter'],\n                ['float-end', 'justifyRight'],\n            ];\n            commandState = (justifyMapping.find(pair => (\n                this.lastMediaClicked.classList.contains(pair[0]))\n            ) || [])[1];\n            resetAlignment = !commandState;\n        }\n        let newClass;\n        if (commandState) {\n            const direction = commandState.replace('justify', '').toLowerCase();\n            newClass = `fa-align-${direction === 'full' ? 'justify' : direction}`;\n            resetAlignment = !['float-start', 'mx-auto', 'float-end'].some(className => (\n                this.lastMediaClicked.classList.contains(className)\n            ));\n        }\n        for (const button of this.toolbarEl.querySelectorAll('#justify div.btn')) {\n            button.classList.toggle('active', !resetAlignment && button.id === commandState);\n        }\n        $paragraphDropdownButton.removeClass((index, className) => (\n            (className.match(/(^|\\s)fa-align-\\w+/g) || []).join(' ')\n        ));\n        if (commandState && !resetAlignment) {\n            $paragraphDropdownButton.addClass(newClass);\n        } else {\n            // Ensure we always display an icon in the align toolbar button.\n            $paragraphDropdownButton.addClass('fa-align-justify');\n        }\n    }\n    _updateFaResizeButtons() {\n        if (!this.lastMediaClicked) {\n            return;\n        }\n        const match = this.lastMediaClicked.className.match(/\\s*fa-([0-9]+)x/);\n        const value = match && match[1] ? match[1] : '1';\n        for (const button of this.toolbarEl.querySelectorAll('#fa-resize div')) {\n            button.classList.toggle('active', button.dataset.value === value);\n        }\n    }\n    _getEditorOptions(options) {\n        const finalOptions = {...this.defaultOptions, ...options};\n        // autohideToolbar is true by default (false by default if navbar present).\n        finalOptions.autohideToolbar = typeof finalOptions.autohideToolbar === 'boolean'\n            ? finalOptions.autohideToolbar\n            : !finalOptions.snippets;\n        if (finalOptions.inlineStyle) {\n            finalOptions.dropImageAsAttachment = false;\n        }\n\n        return finalOptions;\n    }\n    _getBannerCommand(title, emoji, alertClass, iconClass, description, priority) {\n        return {\n            category: _t('Banners'),\n            name: title,\n            priority: priority,\n            description: description,\n            fontawesome: iconClass,\n            isDisabled: () => isSelectionInSelectors('.o_editor_banner') || !this.odooEditor.isSelectionInBlockRoot(),\n            callback: () => {\n                const bannerElement = parseHTML(this.odooEditor.document, `\n                    <div class=\"o_editor_banner o_not_editable lh-1 d-flex align-items-center alert alert-${alertClass} pb-0 pt-3\" role=\"status\" data-oe-protected=\"true\">\n                        <i class=\"o_editor_banner_icon mb-3 fst-normal\" aria-label=\"${_t(title)}\">${emoji}</i>\n                        <div class=\"w-100 px-3\" data-oe-protected=\"false\">\n                            <p><br></p>\n                        </div>\n                    </div>\n                `).childNodes[0];\n                this.odooEditor.execCommand('insert', bannerElement);\n                this.odooEditor.activateContenteditable();\n                setSelection(bannerElement.querySelector('.o_editor_banner > div > p'), 0);\n            },\n        }\n    }\n    async _insertSnippetMenu() {\n        const snippetsMenuMountedProm = new Deferred();\n        this.state.snippetsMenuMountedProm = snippetsMenuMountedProm;\n        this.state.showSnippetsMenu = true;\n        await snippetsMenuMountedProm;\n    }\n    /**\n     * If the element holds a translation, saves it. Otherwise, fallback to the\n     * standard saving but with the lang kept.\n     *\n     * @override\n     */\n    _saveTranslationElement($el, context, withLang = true) {\n        if ($el.data('oe-translation-source-sha')) {\n            const $els = $el;\n            const translations = {};\n            translations[context.lang] = Object.assign({}, ...$els.toArray().map(\n                (x) => ({\n                    [$(x).data('oe-translation-source-sha')]: this._getEscapedElement($(x)).html()\n                })\n            ));\n            return this.orm.call(\n                $els.data('oe-model'),\n                'web_update_field_translations',\n                [\n                    [+$els.data('oe-id')],\n                    $els.data('oe-field'),\n                    translations,\n                ], { context });\n        } else {\n            var viewID = $el.data('oe-id');\n            if (!viewID) {\n                return Promise.resolve();\n            }\n\n            return this.orm.call(\n                'ir.ui.view',\n                'save',\n                [\n                    viewID,\n                    this._getEscapedElement($el).prop('outerHTML'),\n                    !$el.data('oe-expression') && $el.data('oe-xpath') || null, // Note: hacky way to get the oe-xpath only if not a t-field\n                ], { context }\n            );\n        }\n    }\n    _getPowerboxOptions() {\n        const editorOptions = this.options;\n        const categories = [{ name: _t('Banners'), priority: 65 },];\n        const commands = [\n            this._getBannerCommand(_t('Banner Info'), '\ud83d\udca1', 'info', 'fa-info-circle', _t('Insert an info banner'), 24),\n            this._getBannerCommand(_t('Banner Success'), '\u2705', 'success', 'fa-check-circle', _t('Insert a success banner'), 23),\n            this._getBannerCommand(_t('Banner Warning'), '\u26a0\ufe0f', 'warning', 'fa-exclamation-triangle', _t('Insert a warning banner'), 22),\n            this._getBannerCommand(_t('Banner Danger'), '\u274c', 'danger', 'fa-exclamation-circle', _t('Insert a danger banner'), 21),\n            {\n                category: _t('Structure'),\n                name: _t('Quote'),\n                priority: 30,\n                description: _t('Add a blockquote section'),\n                fontawesome: 'fa-quote-right',\n                isDisabled: () => !this.odooEditor.isSelectionInBlockRoot(),\n                callback: () => {\n                    this.odooEditor.execCommand('setTag', 'blockquote');\n                },\n            },\n            {\n                category: _t('Structure'),\n                name: _t('Code'),\n                priority: 20,\n                description: _t('Add a code section'),\n                fontawesome: 'fa-code',\n                isDisabled: () => !this.odooEditor.isSelectionInBlockRoot(),\n                callback: () => {\n                    this.odooEditor.execCommand('setTag', 'pre');\n                },\n            },\n            {\n                category: _t('Basic blocks'),\n                name: _t('Signature'),\n                description: _t('Insert your signature'),\n                fontawesome: 'fa-pencil-square-o',\n                isDisabled: () => !this.odooEditor.isSelectionInBlockRoot(),\n                callback: async () => {\n                    const [currentUser] = await this.orm.read(\n                        'res.users',\n                        [this.user.userId],\n                        ['signature'],\n                    );\n                    if (currentUser && currentUser.signature) {\n                        this.odooEditor.execCommand('insert', parseHTML(this.odooEditor.document, currentUser.signature));\n                    }\n                },\n            },\n            {\n                category: _t('AI Tools'),\n                name: _t('ChatGPT'),\n                description: _t('Generate or transform content with AI.'),\n                fontawesome: 'fa-magic',\n                priority: 1,\n                isDisabled: () => !this.odooEditor.isSelectionInBlockRoot(),\n                callback: async () => this.openChatGPTDialog(),\n            },\n        ];\n        if (!editorOptions.inlineStyle) {\n            commands.push(\n                {\n                    category: _t('Structure'),\n                    name: _t('2 columns'),\n                    priority: 13,\n                    description: _t('Convert into 2 columns'),\n                    fontawesome: 'fa-columns',\n                    callback: () => this.odooEditor.execCommand('columnize', 2, editorOptions.insertParagraphAfterColumns),\n                    isDisabled: () => {\n                        if (!this.odooEditor.isSelectionInBlockRoot()) {\n                            return true;\n                        }\n                        const anchor = this.odooEditor.document.getSelection().anchorNode;\n                        const row = closestElement(anchor, '.o_text_columns .row');\n                        return row && row.childElementCount === 2;\n                    },\n                },\n                {\n                    category: _t('Structure'),\n                    name: _t('3 columns'),\n                    priority: 12,\n                    description: _t('Convert into 3 columns'),\n                    fontawesome: 'fa-columns',\n                    callback: () => this.odooEditor.execCommand('columnize', 3, editorOptions.insertParagraphAfterColumns),\n                    isDisabled: () => {\n                        if (!this.odooEditor.isSelectionInBlockRoot()) {\n                            return true;\n                        }\n                        const anchor = this.odooEditor.document.getSelection().anchorNode;\n                        const row = closestElement(anchor, '.o_text_columns .row');\n                        return row && row.childElementCount === 3;\n                    },\n                },\n                {\n                    category: _t('Structure'),\n                    name: _t('4 columns'),\n                    priority: 11,\n                    description: _t('Convert into 4 columns'),\n                    fontawesome: 'fa-columns',\n                    callback: () => this.odooEditor.execCommand('columnize', 4, editorOptions.insertParagraphAfterColumns),\n                    isDisabled: () => {\n                        if (!this.odooEditor.isSelectionInBlockRoot()) {\n                            return true;\n                        }\n                        const anchor = this.odooEditor.document.getSelection().anchorNode;\n                        const row = closestElement(anchor, '.o_text_columns .row');\n                        return row && row.childElementCount === 4;\n                    },\n                },\n                {\n                    category: _t('Structure'),\n                    name: _t('Remove columns'),\n                    priority: 10,\n                    description: _t('Back to one column'),\n                    fontawesome: 'fa-columns',\n                    callback: () => this.odooEditor.execCommand('columnize', 0),\n                    isDisabled: () => {\n                        if (!this.odooEditor.isSelectionInBlockRoot()) {\n                            return true;\n                        }\n                        const anchor = this.odooEditor.document.getSelection().anchorNode;\n                        const row = closestElement(anchor, '.o_text_columns .row');\n                        return !row;\n                    },\n                },\n                {\n                    category: _t('Widgets'),\n                    name: _t('Emoji'),\n                    priority: 70,\n                    description: _t('Add an emoji'),\n                    fontawesome: 'fa-smile-o',\n                    callback: () => {\n                        this.showEmojiPicker();\n                    },\n                },\n            );\n        }\n        if (editorOptions.allowCommandLink) {\n            categories.push({ name: _t('Navigation'), priority: 40 });\n            commands.push(\n                {\n                    category: _t('Navigation'),\n                    name: _t('Link'),\n                    priority: 40,\n                    description: _t('Add a link'),\n                    fontawesome: 'fa-link',\n                    callback: () => {\n                        this.toggleLinkTools({forceDialog: true});\n                    },\n                },\n                {\n                    category: _t('Navigation'),\n                    name: _t('Button'),\n                    priority: 30,\n                    description: _t('Add a button'),\n                    fontawesome: 'fa-link',\n                    callback: () => {\n                        this.toggleLinkTools({forceDialog: true});\n                        // Force the button style after the link modal is open.\n                        setTimeout(() => {\n                            const selectEl = document.querySelector('.o_link_dialog .form-select');\n                            selectEl.value = 'primary';\n                            // Dynamically changing value of select option\n                            // doesn't trigger a change event. Trigger\n                            // listeners by dispatching the event manually.\n                            selectEl.dispatchEvent(new Event('change'));\n                        }, 150);\n                    },\n                },\n            );\n        }\n        if (editorOptions.allowCommandImage || editorOptions.allowCommandVideo) {\n            categories.push({ name: _t('Media'), priority: 50 });\n        }\n        if (editorOptions.allowCommandImage) {\n            commands.push({\n                category: _t('Media'),\n                name: _t('Image'),\n                priority: 40,\n                description: _t('Insert a picture'),\n                fontawesome: 'fa-file-image-o',\n                callback: () => {\n                    this.openMediaDialog();\n                },\n            });\n        }\n        if (editorOptions.allowCommandVideo) {\n            commands.push({\n                category: _t('Media'),\n                name: _t('Video'),\n                priority: 30,\n                description: _t('Insert a video'),\n                fontawesome: 'fa-file-video-o',\n                callback: () => {\n                    this.openMediaDialog({noVideos: false, noImages: true, noIcons: true, noDocuments: true});\n                },\n            });\n        }\n        if (editorOptions.powerboxCategories) {\n            categories.push(...editorOptions.powerboxCategories);\n        }\n        if (editorOptions.powerboxItems) {\n            commands.push(...editorOptions.powerboxItems);\n        }\n        return {commands, categories};\n    }\n\n    /**\n     * Returns the editable areas on the page.\n     *\n     * @returns {jQuery}\n     */\n    editable() {\n        return $('#wrapwrap [data-oe-model]')\n            .not('.o_not_editable')\n            .filter(function () {\n                return !$(this).closest('.o_not_editable').length;\n            })\n            .not('link, script')\n            .not('[data-oe-readonly]')\n            .not('img[data-oe-field=\"arch\"], br[data-oe-field=\"arch\"], input[data-oe-field=\"arch\"]')\n            .not('.oe_snippet_editor')\n            .add('.o_editable');\n    }\n\n    /**\n     * Searches all the dirty element on the page or given element and saves them one by one. If\n     * one cannot be saved, this notifies it to the user and restarts rte\n     * edition.\n     *\n     * @param {Object} [context] - the context to use for saving rpc, default to\n     *                           the editor context found on the page\n     * @param {Object} [element] - Specific given element to save\n     * @return {Promise} rejected if the save cannot be done\n     */\n    _saveViewBlocks(context, element = false) {\n        // TODO should be review to probably not search in the whole body,\n        // iframe or not.\n        // If the element is given, then search within not from the document.\n        const $ = element ? getJqueryFromDocument(element) : getJqueryFromDocument(this.$editable[0].ownerDocument);\n        const $allBlocks = $((this.options || {}).savableSelector).filter(\n            this.options.enableTranslation\n            ? '.o_dirty, .o_delay_translation'\n            : '.o_dirty');\n\n        const $dirty = $('.o_dirty');\n        $dirty\n            .removeAttr('contentEditable')\n            .removeClass('o_dirty oe_carlos_danger o_is_inline_editable');\n\n        const $delay_translation = $('.o_delay_translation');\n        $delay_translation.removeClass('o_delay_translation');\n\n        $('.o_editable')\n            .removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed');\n\n        const saveElementFuncName = this.options.enableTranslation\n            ? '_saveTranslationElement'\n            : '_saveElement';\n\n        // Group elements to save if possible.\n        const groupedElements = groupBy($allBlocks.toArray(), el => {\n            const model = el.dataset.oeModel;\n            const field = el.dataset.oeField;\n\n            // There are elements which have no linked model as something\n            // special is to be done \"to save them\" (potential override to\n            // `_saveElement` which is expected to be called for each unique\n            // dirty element). In that case, do not group those elements.\n            if (!model) {\n                return uniqueId(\"special-element-to-save-\");\n            }\n\n            // Do not group elements which are parts of views, unless we are\n            // in translate mode.\n            if (!this.options.enableTranslation\n                    && (model === 'ir.ui.view' && field === 'arch')) {\n                return uniqueId(\"view-part-to-save-\");\n            }\n\n            // Otherwise, group elements which are from the same field of the\n            // same record (`_saveElement` will only consider the first one and\n            // `_saveTranslationElement` can handle the set if it makes sense).\n            return `${model}::${el.dataset.oeId}::${field}`;\n        });\n        const proms = Object.values(groupedElements).map(els => {\n            const $els = $(els);\n\n            $els.find('[class]').filter(function () {\n                if (!this.getAttribute('class').match(/\\S/)) {\n                    this.removeAttribute('class');\n                }\n            });\n\n            // TODO: Add a queue with concurrency limit in webclient\n            return new Promise((resolve, reject) => {\n                return this.saving_mutex.exec(() => {\n                    return this[saveElementFuncName]($els, context || this.options.context)\n                    .then(function () {\n                        $els.removeClass('o_dirty');\n                        resolve();\n                    })\n                    .catch(error => {\n                        // because ckeditor regenerates all the dom, we can't just\n                        // setup the popover here as everything will be destroyed by\n                        // the DOM regeneration. Add markings instead, and returns a\n                        // new rejection with all relevant info\n                        var id = uniqueId(\"carlos_danger_\");\n                        $els.addClass('o_dirty o_editable oe_carlos_danger ' + id);\n                        $('.o_editable.' + id)\n                            .removeClass(id)\n                            .popover({\n                                trigger: 'hover',\n                                content: error.data?.message || '',\n                                placement: 'auto',\n                            })\n                            .popover('show');\n                        reject();\n                    });\n                });\n            });\n        });\n        return Promise.all(proms).then(function () {\n            window.onbeforeunload = null;\n        });\n    }\n    // TODO unused => remove or reuse as it should be\n    _attachTooltips() {\n        $(document.body)\n            .tooltip({\n                selector: '[data-oe-readonly]',\n                container: 'body',\n                trigger: 'hover',\n                delay: {'show': 1000, 'hide': 100},\n                placement: 'bottom',\n                title: _t(\"Readonly field\")\n            })\n            .on('click', function () {\n                $(this).tooltip('hide');\n            });\n    }\n    /**\n     * Gets jQuery cloned element with internal text nodes escaped for XML\n     * storage.\n     *\n     * @private\n     * @param {jQuery} $el\n     * @return {jQuery}\n     */\n    _getEscapedElement($el) {\n        var escaped_el = $el.clone();\n        var to_escape = escaped_el.find('*').addBack();\n        to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!=\"ir.ui.view\"]').find('*').addBack());\n        to_escape.contents().each(function () {\n            if (this.nodeType === 3) {\n                this.nodeValue = $('<div />').text(this.nodeValue).html();\n            }\n        });\n        return escaped_el;\n    }\n    /**\n     * Saves one (dirty) element of the page.\n     *\n     * @private\n     * @param {jQuery} $el - the element to save\n     * @param {Object} context - the context to use for the saving rpc\n     */\n    async _saveElement($el, context) {\n        var viewID = $el.data('oe-id');\n        if (!viewID) {\n            return Promise.resolve();\n        }\n\n        // remove ZeroWidthSpace from odoo field value\n        // ZeroWidthSpace may be present from OdooEditor edition process\n        let escapedHtml = this._getEscapedElement($el).prop('outerHTML');\n\n        const result = this.orm.call('ir.ui.view', 'save', [\n            viewID,\n            escapedHtml,\n            !$el.data('oe-expression') && $el.data('oe-xpath') || null\n        ], {\n            context: {\n                ...context,\n                // TODO: Restore the delay translation feature once it's fixed,\n                //       see commit msg for more info.\n                delay_translations: false,\n            },\n        });\n        return result;\n    }\n\n    /**\n     * Reloads the page in non-editable mode, with the right scrolling.\n     *\n     * @private\n     * @returns {Promise} (never resolved, the page is reloading anyway)\n     */\n    _reload() {\n        window.location.search = 'scrollTop=' + window.document.body.scrollTop;\n        if (window.location.search.indexOf('enable_editor') >= 0) {\n            window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');\n        } else {\n            window.location.reload(true);\n        }\n        return new Promise(function () {});\n    }\n    _onAttachmentChange(attachment) {\n        if (this.options.onAttachmentChange) {\n            this.options.onAttachmentChange(attachment);\n        }\n    }\n    _onDblClickEditableMedia(ev) {\n        const $el = $(ev.currentTarget);\n        $el.selectElement();\n        if (!$el.parent().hasClass('o_stars')) {\n            // Waiting for all the options to be initialized before\n            // opening the media dialog and only if the media has not\n            // been deleted in the meantime.\n            this.waitForEmptyMutexAction().then(() => {\n                if ($el[0].parentElement) {\n                    this.openMediaDialog({ node: $el[0] });\n                }\n            });\n        }\n    }\n    _onSelectionChange() {\n        if (this.odooEditor.autohideToolbar && this.linkPopover) {\n            const selectionInLink = getInSelection(this.odooEditor.document, 'a') === this.linkPopover.target;\n            const isVisible = this.linkPopover.el.offsetParent;\n            if (isVisible && !selectionInLink) {\n                this.linkPopover.hide();\n            }\n        }\n    }\n\n    _getDelayBlurSelectors() {\n        return [\".oe-toolbar\", \".oe-powerbox-wrapper\", \".o_we_crop_widget\"];\n    }\n\n    _onDocumentMousedown(e) {\n        if (!e.target.classList.contains('o_editable_date_field_linked')) {\n            this.$editable.find('.o_editable_date_field_linked').removeClass('o_editable_date_field_linked');\n        }\n        const closestDialog = e.target.closest('.o_dialog, .o_web_editor_dialog');\n        if (\n            e.target.closest(\"#oe_snippets\") ||\n            e.target.closest(this._getDelayBlurSelectors().join(\",\")) ||\n            (closestDialog && closestDialog.querySelector('.o_select_media_dialog, .o_link_dialog'))\n        ) {\n            this._shouldDelayBlur = true;\n        } else {\n            if (this._pendingBlur && !e.target.closest('.o_wysiwyg_wrapper')) {\n                this.options.onWysiwygBlur && this.options.onWysiwygBlur();\n                this._pendingBlur = false;\n            }\n            this._shouldDelayBlur = false;\n        }\n    }\n    _onBlur() {\n        if (this._shouldDelayBlur) {\n            this._pendingBlur = true;\n        } else {\n            this.options.onWysiwygBlur && this.options.onWysiwygBlur();\n        }\n    }\n    _onScroll(ev) {\n        if (ev.target.contains(this.$editable[0])) {\n            this.scrollContainer = ev.target;\n            this.odooEditor.updateToolbarPosition();\n        }\n    }\n    _signalOffline() {\n        this._isOnline = false;\n    }\n    async _signalOnline() {\n        clearTimeout(this._offlineTimeout);\n        this._offlineTimeout = undefined;\n\n        if (this._isOnline || !navigator.onLine) {\n            return;\n        }\n        this._isOnline = true;\n        if (!this.ptp) return;\n\n        // If it was disconnected to some peers, send the join signal again.\n        this.ptp.notifyAllClients('ptp_join');\n        // Send last step to all peers. If the peers cannot add the step, they\n        // will ask for missing steps.\n        this.ptp.notifyAllClients('oe_history_step', peek(this.odooEditor.historyGetSteps()), { transport: 'rtc' });\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.clientId has a stale document\n        // - the step.clientId has a snapshot and does not includes the step in\n        //   its history\n        // - if another share history id\n        //   - because the step.clientId has reset from the server and\n        //     step.clientId is not synced with this client\n        //   - because the step.clientId is in a network partition\n        if (missingSteps === -1 || !missingSteps.length) {\n            return false;\n        }\n        this.ptp && this.odooEditor.onExternalHistorySteps(missingSteps);\n        return true;\n    }\n    _showConflictDialog() {\n        if (this._conflictDialogOpened) return;\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    _getLastHistoryStepId(value) {\n        const matchId = value.match(/data-last-history-steps=\"(?:[0-9]+,)*([0-9]+)\"/);\n        return matchId && matchId[1];\n    }\n    _generateClientId() {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    }\n    _getNewPtp() {\n        const rpcMutex = new Mutex();\n        const {collaborationChannel} = this.options;\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            currentClientId: this._currentClientId,\n            broadcastAll: (rpcData) => {\n                return rpcMutex.exec(async () => {\n                    return this._serviceRpc('/web_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_start_time: () => this._startCollaborationTime,\n                get_client_name: () => user.name,\n                get_client_avatar: () => `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(user.userId)}`,\n                get_missing_steps: (params) => this.odooEditor.historyGetMissingSteps(params.requestPayload),\n                get_history_from_snapshot: () => this._getHistorySnapshot(),\n                get_collaborative_selection: () => this.odooEditor.getCurrentCollaborativeSelection(),\n                recover_document: (params) => {\n                    const { serverDocumentId, fromStepId } = params.requestPayload;\n                    if (!this.odooEditor.historyGetBranchIds().includes(serverDocumentId)) {\n                        return;\n                    }\n                    return {\n                        missingSteps: this.odooEditor.historyGetMissingSteps({ fromStepId }),\n                        snapshot: this._getHistorySnapshot(),\n                    };\n                },\n            },\n            onNotification: async ({ fromClientId, notificationName, notificationPayload }) => {\n                switch (notificationName) {\n                    case 'ptp_remove':\n                        this.odooEditor.multiselectionRemove(notificationPayload);\n                        break;\n                    case 'ptp_disconnect':\n                        this.ptp.removeClient(fromClientId);\n                        this.odooEditor.multiselectionRemove(fromClientId);\n                        break;\n                    case 'rtc_data_channel_open': {\n                        fromClientId = notificationPayload.connectionClientId;\n                        const remoteStartTime = await this.requestClient(fromClientId, 'get_start_time', undefined, { transport: 'rtc' });\n                        if (remoteStartTime === REQUEST_ERROR) return;\n                        this.ptp.clientsInfos[fromClientId].startTime = remoteStartTime;\n\n                        if (!this._historySyncAtLeastOnce) {\n                            const localClient = { id: this._currentClientId, startTime: this._startCollaborationTime };\n                            const remoteClient = { id: fromClientId, startTime: remoteStartTime };\n                            if (isClientFirst(localClient, remoteClient)) {\n                                this._historySyncAtLeastOnce = true;\n                                this._historySyncFinished = true;\n                            } else {\n                                this._resetCollabRequests();\n                                const response = await this._resetFromClient(fromClientId, this._lastCollaborationResetId);\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.notifyAllClients('oe_history_step', peek(this.odooEditor.historyGetSteps()), { transport: 'rtc' });\n                            this._setCollaborativeSelection(fromClientId);\n                        }\n\n                        const getClientNamePromise = this.requestClient(\n                            fromClientId, 'get_client_name', undefined, { transport: 'rtc' }\n                        ).then((clientName) => {\n                            if (clientName === REQUEST_ERROR) return;\n                            this.ptp.clientsInfos[fromClientId].clientName = clientName;\n                            this.odooEditor.multiselectionRefresh();\n                        });\n                        const getClientAvatar = this.requestClient(\n                            fromClientId, 'get_client_avatar', undefined, { transport: 'rtc' }\n                        ).then(clientAvatarUrl => {\n                            if (clientAvatarUrl === REQUEST_ERROR) return;\n                            this.ptp.clientsInfos[fromClientId].clientAvatarUrl = clientAvatarUrl;\n                            this.odooEditor.multiselectionRefresh();\n                        });\n                        await Promise.all([getClientAvatar, getClientNamePromise]);\n                        break;\n                    }\n                    case 'oe_history_step':\n                        if (this._historySyncFinished) {\n                            this.odooEditor.onExternalHistorySteps([notificationPayload]);\n                        } else {\n                            this._historyStepsBuffer.push(notificationPayload);\n                        }\n                        break;\n                    case 'oe_history_set_selection': {\n                        const client = this.ptp.clientsInfos[fromClientId];\n                        if (!client) {\n                            return;\n                        }\n                        const selection = notificationPayload;\n                        selection.clientName = client.clientName;\n                        selection.clientAvatarUrl = client.clientAvatarUrl;\n                        this.odooEditor.onExternalMultiselectionUpdate(selection);\n                        break;\n                    }\n                }\n            }\n        });\n    }\n    _getCollaborationClientAvatarUrl() {\n        return `${browser.location.origin}/web/image?model=res.users&field=avatar_128&id=${encodeURIComponent(user.userId)}`\n    }\n    _stopPeerToPeer() {\n        this._joiningPtp = false;\n        this._ptpJoined = false;\n        this._resetCollabRequests();\n        this.ptp && this.ptp.stop();\n    }\n    _joinPeerToPeer() {\n        this.$editable[0].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._resetFromServerAndResyncWithClients();\n                    if (!success) return;\n                }\n                this.ptp.notifyAllClients('ptp_join');\n                this._joiningPtp = false;\n                this._ptpJoined = true;\n            });\n        }\n    }\n    async _setCollaborativeSelection(fromClientId) {\n        const remoteSelection = await this.requestClient(fromClientId, 'get_collaborative_selection', undefined, { transport: 'rtc' });\n        if (remoteSelection === REQUEST_ERROR) return;\n        if (remoteSelection) {\n            this.odooEditor.onExternalMultiselectionUpdate(remoteSelection);\n        }\n    }\n    /**\n     * Get peer to peer clients.\n     */\n    _getPtpClients() {\n        const clients = Object.entries(this.ptp.clientsInfos).map(([clientId, clientInfo]) => ({id: clientId, ...clientInfo}));\n        return clients.sort((a, b) => isClientFirst(a, b) ? -1 : 1);\n    }\n    async _getCurrentRecord() {\n        const [record] = await this.orm.read(\n            this.options.collaborationChannel.collaborationModelName,\n            [this.options.collaborationChannel.collaborationResId],\n            [this.options.collaborationChannel.collaborationFieldName],\n        );\n        return record;\n    }\n    _isLastDocumentStale() {\n        if (!this._serverLastStepId) {\n            return false;\n        }\n        return !this.odooEditor.historyGetBranchIds().includes(this._serverLastStepId);\n    }\n    /**\n     * Update the server document last step id and recover from a stale document\n     * if this client 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     * 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._getPtpClients().map(client => client.id);\n\n            if (allPeers.length === 0) {\n                if (this._isDocumentStale) {\n                    this._showConflictDialog();\n                    resolve();\n                    return this._resetFromServerAndResyncWithClients();\n                }\n            }\n\n            let hasRetrievalBudgetTimeout = false;\n            let 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.requestClient(\n                    peerId,\n                    'recover_document', {\n                        serverDocumentId: this._serverLastStepId,\n                        fromStepId: peek(this.odooEditor.historyGetBranchIds()),\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._resetFromServerAndResyncWithClients();\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) return;\n                hasRetrievalBudgetTimeout = true;\n                this._onRecoveryClientTimeout(processSnapshots);\n            }, PTP_MAX_RECOVERY_TIME);\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 _onRecoveryClientTimeout(processSnapshots) {\n        processSnapshots();\n    }\n    /**\n     * Reset the document from the server and resync with the clients.\n     */\n    async _resetFromServerAndResyncWithClients() {\n        let collaborationResetId = this._lastCollaborationResetId;\n        const record = await this._getCurrentRecord();\n        if (collaborationResetId !== this._lastCollaborationResetId) return;\n\n        const content = record[this.options.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('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        this._isDocumentStale = false;\n        this.resetValue(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 client 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(this._getPtpClients().map((client) => {\n            // Reset from the fastest client. The first client to reset will set\n            // this._historySyncAtLeastOnce to true canceling the other peers\n            // resets.\n            return this._resetFromClient(client.id, collaborationResetId);\n        }));\n        return true;\n    }\n    _resetCollabRequests() {\n        this._lastCollaborationResetId++;\n        // By aborting the current requests from ptp, we ensure that the ongoing\n        // `Wysiwyg.requestClient` will return REQUEST_ERROR. Most requests that\n        // calls `Wysiwyg.requestClient` might want to check if the response is\n        // REQUEST_ERROR.\n        this.ptp && this.ptp.abortCurrentRequests();\n    }\n    async _resetFromClient(fromClientId, resetCollabCount) {\n        this._historySyncFinished = false;\n        this._historyStepsBuffer = [];\n        const snapshot = await this.requestClient(fromClientId, 'get_history_from_snapshot', undefined, { transport: 'rtc' });\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 client before\n        // this `get_history_from_snapshot` finished.\n        if (this._historySyncAtLeastOnce) {\n            return;\n        }\n        const applied = this._applySnapshot(snapshot);\n        if (!applied) {\n            return;\n        }\n        this._historySyncFinished = true;\n        // In case there are steps received in the meantime, process them.\n        if (this._historyStepsBuffer.length) {\n            this.odooEditor.onExternalHistorySteps(this._historyStepsBuffer);\n            this._historyStepsBuffer = [];\n        }\n        this.options.onHistoryResetFromSteps();\n        this._setCollaborativeSelection(fromClientId);\n    }\n    async requestClient(clientId, requestName, requestPayload, params) {\n        return this.ptp.requestClient(clientId, requestName, requestPayload, params).catch((e) => {\n            if (e instanceof RequestError) {\n                return REQUEST_ERROR;\n            } else {\n                throw e;\n            }\n        });\n    }\n    /**\n     * Reset the value and history of the editor.\n     */\n    async resetValue(value) {\n        this.setValue(value);\n        this.odooEditor.historyReset();\n        this._historyShareId = Math.floor(Math.random() * Math.pow(2,52)).toString();\n        this._serverLastStepId = value && this._getLastHistoryStepId(value);\n        if (this._serverLastStepId) {\n            this.odooEditor.historySetInitialId(this._serverLastStepId);\n        }\n    }\n    /**\n     * Reset the editor with a new value and potientially new options.\n     */\n    async resetEditor(value, options) {\n        await this._peerToPeerLoading;\n        this.$editable[0].removeEventListener('focus', this._joinPeerToPeer);\n        if (options) {\n            this.options = this._getEditorOptions(options);\n        }\n        const {collaborationChannel} = this.options;\n        this._stopPeerToPeer();\n        this._collaborationStopBus && this._collaborationStopBus();\n        this._isDocumentStale = false;\n        this._rulesCache = undefined; // Reset the cache of rules.\n        // If there is no collaborationResId, the record has been deleted.\n        if (!this._isCollaborationEnabled(this.options)) {\n            this._currentClientId = undefined;\n            this.resetValue(value);\n            return;\n        }\n        this._currentClientId = this._generateClientId();\n        this.odooEditor.collaborationSetClientId(this._currentClientId);\n        this.resetValue(value);\n        this.setupCollaboration(collaborationChannel);\n        if (this.options.collaborativeTrigger === 'start') {\n            this._joinPeerToPeer();\n        } else if (this.options.collaborativeTrigger === 'focus') {\n            // Wait until editor is focused to join the peer to peer network.\n            this.$editable[0].addEventListener('focus', this._joinPeerToPeer);\n        }\n\n        await this._peerToPeerLoading;\n    }\n    _getHistorySnapshot() {\n        return Object.assign(\n            {},\n            this.odooEditor.historyGetSnapshotSteps(),\n            { historyShareId: this._historyShareId }\n        );\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 = this._serverLastStepId && !historyIds.includes(this._serverLastStepId);\n        if (isStaleDocument) {\n            return;\n        }\n        this._historyShareId = historyShareId;\n        this._historySyncAtLeastOnce = true;\n        this.odooEditor.historyResetFromSteps(steps, historyIds);\n        this.odooEditor.historyResetLatestComputedSelection();\n        return true;\n    }\n    /**\n     * Set `contenteditable` according to `.o_not_editable` and `.o_editable`.\n     *\n     * @param {Node} node\n     */\n    _onPostSanitize(node) {\n        // _fixLinkMutatedElements check to be removed after the new link edge\n        // solution is merged.\n        if (node?.querySelectorAll && this.odooEditor && !this.odooEditor._fixLinkMutatedElements) {\n            // TODO rethink o_editable as a content-editable marker without\n            // breaking the o_editable behaviors (website, mass_mailing, ...)\n            for (const element of node.querySelectorAll('.o_not_editable')) {\n                if (element.isContentEditable !== false) {\n                    element.contentEditable = false;\n                }\n            }\n        }\n    }\n    _attachHistoryIds(editable = this.odooEditor.editable) {\n        if (this.options.collaborative) {\n            // clean existig 'data-last-history-steps' attributes\n            editable.querySelectorAll('[data-last-history-steps]').forEach(\n                el => el.removeAttribute('data-last-history-steps')\n            );\n\n            const historyIds = this.odooEditor.historyGetBranchIds().join(',');\n            const firstChild = editable.children[0];\n            if (firstChild) {\n                firstChild.setAttribute('data-last-history-steps', historyIds);\n            }\n        }\n    }\n    _bindOnBlur() {\n        this.$editable.on('blur', this._onBlur);\n    }\n\n    _hasICEServers() {\n        // Hack: check if mail module is installed.\n        return this.env.services['mail.store'];\n    }\n    _isCollaborationEnabled(options) {\n        return options.collaborationChannel && options.collaborationChannel.collaborationResId && this._hasICEServers() && this.busService;\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        el.classList.remove('o_b64_image_to_save');\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            return;\n        }\n        const attachment = await this._serviceRpc(\n            '/web_editor/attachment/add_data',\n            {\n                name: el.dataset.fileName || '',\n                data: imageData,\n                is_image: true,\n                res_model: resModel,\n                res_id: resId,\n            },\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            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.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    }\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(image, 0, 0, image.width, image.height, 0, 0, canvas.width, canvas.height);\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.toDataURL('image/webp', 0.75).split(',')[1];\n                }\n            }\n        }\n        const newAttachmentSrc = await this._serviceRpc(\n            `/web_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 ? el.dataset.mimetype : 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 = weUtils.backgroundImageCssToParts($(el).css('background-image'));\n            parts.url = `url('${newAttachmentSrc}')`;\n            const combined = weUtils.backgroundImagePartsToCss(parts);\n            $(el).css('background-image', combined);\n            delete el.dataset.bgSrc;\n        } else {\n            el.setAttribute('src', newAttachmentSrc);\n            // Also update carousel thumbnail.\n            weUtils.forwardToThumbnail(el);\n        }\n    }\n\n    /**\n     * @private\n     */\n    _beforeAnyCommand() {\n        // Remove any marker of default text in the selection on which the\n        // command is being applied. Note that this needs to be done *before*\n        // the command and not after because some commands (e.g. font-size)\n        // rely on some elements not to have the class to fully work.\n        for (const node of OdooEditorLib.getTraversedNodes(this.$editable[0])) {\n            const el = node.nodeType === Node.TEXT_NODE ? node.parentElement : node;\n            const defaultTextEl = el.closest('.o_default_snippet_text');\n            if (defaultTextEl) {\n                defaultTextEl.classList.remove('o_default_snippet_text');\n            }\n        }\n    }\n\n    // -----------------------------------------------------------------------------\n    // Legacy compatibility layer\n    // Remove me when all legacy widgets using wysiwyg are converted to OWL.\n    // -----------------------------------------------------------------------------\n    _trigger_up(ev) {\n        const evType = ev.name;\n        const payload = ev.data;\n        if (evType === 'call_service') {\n            this._callService(payload);\n        }\n    }\n    _callService(payload) {\n        const service = this.env.services[payload.service];\n        const result = service[payload.method].apply(service, payload.args || []);\n        payload.callback(result);\n    }\n    _serviceRpc(route, params, settings = {}) {\n        if (status(this) === \"destroyed\") {\n            return;\n        }\n        if (params && params.kwargs) {\n            params.kwargs.context = {\n                ...user.context,\n                ...params.kwargs.context,\n            };\n        }\n        return rpc(route, params, {\n            silent: settings.shadow,\n            xhr: settings.xhr,\n        });\n    }\n}\nWysiwyg.activeCollaborationChannelNames = new Set();\nWysiwyg.activeWysiwygs = new Set();\n//--------------------------------------------------------------------------\n// Public helper\n//--------------------------------------------------------------------------\n/**\n * @param {Node} [ownerDocument] (document on which to get the selection)\n * @returns {Object}\n * @returns {Node} sc - start container\n * @returns {Number} so - start offset\n * @returns {Node} ec - end container\n * @returns {Number} eo - end offset\n */\nWysiwyg.getRange = function (ownerDocument) {\n    const selection = (ownerDocument || document).getSelection();\n    if (selection.rangeCount === 0) {\n        return {\n            sc: null,\n            so: 0,\n            ec: null,\n            eo: 0,\n        };\n    }\n    const range = selection.getRangeAt(0);\n\n    return {\n        sc: range.startContainer,\n        so: range.startOffset,\n        ec: range.endContainer,\n        eo: range.endOffset,\n    };\n};\n/**\n * @param {Node} startNode\n * @param {Number} startOffset\n * @param {Node} endNode\n * @param {Number} endOffset\n */\nWysiwyg.setRange = function (startNode, startOffset = 0, endNode = startNode, endOffset = startOffset) {\n    const selection = document.getSelection();\n    selection.removeAllRanges();\n\n    const range = new Range();\n    range.setStart(startNode, startOffset);\n    range.setEnd(endNode, endOffset);\n    selection.addRange(range);\n};\n\n// Check wether clientA is before clientB.\nfunction isClientFirst(clientA, clientB) {\n    if (clientA.startTime === clientB.startTime) {\n        return clientA.id.localeCompare(clientB.id) === -1;\n    } if (clientA.startTime === undefined || clientB.startTime === undefined) {\n        return Boolean(clientA.startTime);\n    } else {\n        return clientA.startTime < clientB.startTime;\n    }\n}\n\nexport function stripHistoryIds(value) {\n    return value && value.replace(/\\sdata-last-history-steps=\"[^\"]*?\"/, '') || value;\n}\n", "/** @odoo-module **/\n\nimport { Wysiwyg } from '@web_editor/js/wysiwyg/wysiwyg';\nimport { patch } from \"@web/core/utils/patch\";\nimport { getBundle } from \"@web/core/assets\";\nimport { isMobileOS } from \"@web/core/browser/feature_detection\";\nimport { useEffect } from \"@odoo/owl\";\n\nvar promiseJsAssets;\n\n/**\n * Add option (inIframe) to load Wysiwyg in an iframe.\n **/\n\npatch(Wysiwyg.prototype, {\n    setup() {\n        super.setup();\n        useEffect(\n            () => this.handleSnippetsDisplay(),\n            () => [this.state.showSnippetsMenu, this.state.snippetsMenuFolded]\n        );\n    },\n    /**\n     * Add options to load Wysiwyg in an iframe.\n     *\n     * @override\n     * @param {boolean} options.inIframe\n     **/\n    init() {\n        super.init();\n        if (this.options.inIframe) {\n            this._onUpdateIframeId = 'onLoad_' + this.id;\n        }\n    },\n    /**\n     * @override\n     **/\n    async startEdition() {\n        if (!this.options.inIframe) {\n            return super.startEdition();\n        } else {\n            this.defAsset = this._getAssets();\n            await this.defAsset;\n            await this._loadIframe();\n            return super.startEdition();\n        }\n    },\n\n    /**\n     * @override\n     **/\n    destroy() {\n        if (this.options.inIframe) {\n            this.$iframe?.[0].contentDocument.removeEventListener('scroll', this._onScroll, true);\n        }\n        super.destroy();\n    },\n\n    /**\n     * Add or remove iframe classes depending on the snippets menu folding\n     * state, in order to be able to add/remove enough blank space for it\n     * through css rules.\n     */\n    handleSnippetsDisplay() {\n        const iframe = this.$iframe?.[0];\n        if (!iframe || !iframe.isConnected) {\n            return;\n        }\n        iframe.classList.toggle(\n            \"has_snippets_sidebar\",\n            this.state.showSnippetsMenu && !this.state.snippetsMenuFolded\n        );\n    },\n\n    /**\n     * Hook called when the wysiwyg fullscreen state changes (allows overrides).\n     *\n     * @param {Boolean} isFullscreen\n     */\n    onToggleFullscreen(isFullscreen) {},\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     **/\n    _getEditorOptions() {\n        const options = super._getEditorOptions(...arguments);\n        if (!(\"getContextFromParentRect\" in options)) {\n            options.getContextFromParentRect = () => {\n                return this.$iframe && this.$iframe.length ? this.$iframe[0].getBoundingClientRect() : { top: 0, left: 0 };\n            };\n        }\n        if (this.$iframe && this.$iframe.length) {\n            options.document = this.$iframe[0].contentWindow.document;\n        }\n        return options;\n    },\n    /**\n     * Create iframe, inject css and create a link with the content,\n     * then inject the target inside.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    _loadIframe() {\n        var self = this;\n        this.$editable = $('<div class=\"note-editable oe_structure odoo-editor-editable\"></div>');\n        this.$el.removeClass('note-editable oe_structure odoo-editor-editable');\n        this.$iframe = $('<iframe class=\"wysiwyg_iframe o_iframe\">').css({\n            width: '100%',\n            height: '100%',\n        });\n        this.handleSnippetsDisplay();\n        var avoidDoubleLoad = 0; // this bug only appears on some configurations.\n\n        // resolve promise on load\n        var def = new Promise(function (resolve) {\n            window.top[self._onUpdateIframeId] = function (_avoidDoubleLoad) {\n                if (_avoidDoubleLoad !== avoidDoubleLoad) {\n                    console.warn('Wysiwyg iframe double load detected');\n                    return;\n                }\n                delete window.top[self._onUpdateIframeId];\n                var $iframeTarget = self.$iframe.contents().find('#iframe_target');\n                // copy the html in itself to have the node prototypes relative\n                // to this window rather than the iframe window.\n                const $targetClone = $iframeTarget.clone();\n                $targetClone.find('script').remove();\n                $iframeTarget.html($targetClone.html());\n                self.$iframeBody = $iframeTarget;\n                $iframeTarget.attr(\"isMobile\", isMobileOS());\n\n                const $iframeWrapper = $('<div class=\"iframe-editor-wrapper odoo-editor\">');\n                const $codeview = $('<textarea class=\"o_codeview d-none\"/>');\n                self.$editable.addClass('o_editable oe_structure');\n\n                $iframeTarget.append($codeview);\n                $iframeTarget.append($iframeWrapper);\n                $iframeWrapper.append(self.$editable);\n\n                self.options.toolbarHandler = $('#web_editor-top-edit', self.$iframe[0].contentWindow.document);\n                resolve();\n            };\n        });\n        this.$iframe.data('loadDef', def); // for unit test\n\n        // inject content in iframe\n\n        this.$iframe.on('load', function onLoad (ev) {\n            var _avoidDoubleLoad = ++avoidDoubleLoad;\n            self.defAsset.then(function (assets) {\n                if (_avoidDoubleLoad !== avoidDoubleLoad) {\n                    console.warn('Wysiwyg immediate iframe double load detected');\n                    return;\n                }\n\n                const iframeContent = getWysiwygIframeContent({\n                    assets: assets,\n                    updateIframeId: self._onUpdateIframeId,\n                    avoidDoubleLoad: _avoidDoubleLoad\n                });\n                self.$iframe[0].contentWindow.document\n                    .open(\"text/html\", \"replace\")\n                    .write(`<!DOCTYPE html><html${\n                        self.options.iframeHtmlClass ? ' class=\"' + self.options.iframeHtmlClass +'\"' : ''\n                    }>${iframeContent}</html>`);\n                // Closing the document might trigger a new 'load' event.\n                self.$iframe.off('load', onLoad);\n                self.$iframe[0].contentWindow.document.close();\n            });\n            self.options.document = self.$iframe[0].contentWindow.document;\n        });\n\n        this.$el.append(this.$iframe);\n\n        return def.then(() => {\n            this.options.onIframeUpdated();\n            this.handleSnippetsDisplay();\n        });\n    },\n\n    async _insertSnippetMenu() {\n        if (this.options.inIframe) {\n            this.el.classList.add(\"w-100\");\n        }\n        return super._insertSnippetMenu();\n    },\n    /**\n     * Get assets for the iframe.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async _getAssets() {\n        promiseJsAssets = promiseJsAssets || await getBundle('web_editor.wysiwyg_iframe_editor_assets');\n        const assetsPromises = [promiseJsAssets];\n        if (this.options.iframeCssAssets) {\n            assetsPromises.push(getBundle(this.options.iframeCssAssets));\n        }\n        return Promise.all(assetsPromises);\n    },\n    /**\n     * Bind the blur event on the iframe so that it would not blur when using\n     * the sidebar.\n     *\n     * @override\n     */\n    _bindOnBlur() {\n        if (!this.options.inIframe) {\n            super._bindOnBlur(...arguments);\n        } else {\n            this.$iframe[0].contentWindow.addEventListener('blur', this._onBlur);\n        }\n    },\n\n    /**\n     * When the editable is inside an iframe, we want to update the toolbar\n     * position in 2 scenarios:\n     * 1. scroll event in the top document, if the iframe is a descendant of\n     * the scroll container.\n     * 2. scroll event in the iframe's document.\n     *\n     * @override\n     */\n    _onScroll(ev) {\n        if (this.options.inIframe) {\n            const iframeDocument = this.$iframe[0].contentDocument;\n            const scrollInIframe = ev.target === iframeDocument || ev.target.ownerDocument === iframeDocument;\n            if (ev.target.contains(this.$iframe[0]))  {\n                this.scrollContainer = ev.target;\n                this.odooEditor.updateToolbarPosition();\n            } else if (scrollInIframe) {\n                // UpdateToolbarPosition needs a scroll container in the top document.\n                this.scrollContainer = this.$iframe[0];\n                this.odooEditor.updateToolbarPosition();\n            }\n        } else {\n            return super._onScroll(...arguments);\n        }\n    },\n\n    /**\n     * @override\n     */\n    _configureToolbar(options) {\n        super._configureToolbar(...arguments);\n        if (this.options.inIframe && !options.snippets) {\n            this.$iframe[0].contentDocument.addEventListener('scroll', this._onScroll, true);\n        }\n    },\n});\n\nfunction getWysiwygIframeContent(params) {\n    const assets = {\n        cssLibs: [],\n        jsLibs: [],\n    };\n    for (const asset of params.assets) {\n        for (const cssLib of asset.cssLibs) {\n            assets.cssLibs.push(`<link type=\"text/css\" rel=\"stylesheet\" href=\"${cssLib}\"/>`);\n        }\n        for (const jsLib of asset.jsLibs) {\n            assets.jsLibs.push(`<script type=\"text/javascript\" src=\"${jsLib}\"/>`);\n        }\n    }\n    return `\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        ${assets.cssLibs.join('\\n')}\n        ${assets.jsLibs.join('\\n')}\n\n        <script type=\"text/javascript\">\n            window.odoo?.define('root.widget', ['@web/legacy/js/public/public_widget'], function (require) {\n                'use strict';\n                const publicWidget = require('@web/legacy/js/public/public_widget')[Symbol.for(\"default\")];\n                const widget = new publicWidget.Widget();\n                widget.appendTo(document.body);\n                return widget;\n            });\n        </script>\n    </head>\n    <body class=\"o_in_iframe\">\n        <div id=\"iframe_target\"/>\n        <script type=\"text/javascript\">\n            window.odoo?.define('web_editor.wysiwyg.iniframe', [], function (require) {\n                'use strict';\n                if (window.top.${params.updateIframeId}) {\n                    window.top.${params.updateIframeId}(${params.avoidDoubleLoad});\n                }\n            });\n        </script>\n    </body>`;\n}\n", "/** @odoo-module **/\n\nimport { LinkDialog } from \"@web_editor/js/wysiwyg/widgets/link_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\nimport wUtils from \"@website/js/utils\";\nimport { useEffect } from '@odoo/owl';\n\npatch(LinkDialog.prototype, {\n    /**\n     * Allows the URL input to propose existing website pages.\n     *\n     * @override\n     */\n    setup() {\n        super.setup();\n        useEffect(($link, container) => {\n            const input = container?.querySelector(`input[name=\"url\"]`);\n            if (!input) {\n                return;\n            }\n            const options = {\n                body: $link && $link[0].ownerDocument.body,\n                urlChosen: () => this.__onURLInput(),\n            };\n            const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(input, options);\n            return () => unmountAutocompleteWithPages();\n        }, () => [this.$link, this.linkComponentWrapperRef.el]);\n    }\n});\n", "import { AddSnippetDialog } from \"@web_editor/js/editor/add_snippet_dialog\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { applyTextHighlight } from \"@website/js/text_processing\";\n\npatch(AddSnippetDialog.prototype, {\n    /**\n     * @override\n     */\n    _updateSnippetContent(targetEl) {\n        super._updateSnippetContent(...arguments);\n        // Build the highlighted text content for the snippets.\n        for (const textEl of targetEl?.querySelectorAll(\".o_text_highlight\") || []) {\n            applyTextHighlight(textEl);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { LinkTools } from '@web_editor/js/wysiwyg/widgets/link_tools';\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { onWillStart, status, useEffect } from '@odoo/owl';\nimport wUtils from \"@website/js/utils\";\nimport { debounce } from \"@web/core/utils/timing\";\nimport { Wysiwyg } from \"@web_editor/js/wysiwyg/wysiwyg\";\n\nconst LINK_DEBOUNCE = 1000;\n\npatch(LinkTools.prototype, {\n    /**\n     * Allows the URL input to propose existing website pages.\n     *\n     * @override\n     */\n    async start() {\n        var def = await super.start(...arguments);\n        this._adaptPageAnchor();\n        return def;\n    },\n\n    setup() {\n        super.setup();\n        onWillStart(() => {\n            this._adaptPageAnchor = debounce(this._adaptPageAnchor, LINK_DEBOUNCE);\n        });\n        useEffect((container) => {\n            const input = container?.querySelector(`input[name=\"url\"]`);\n            if (!input) {\n                return;\n            }\n            const options = {\n                classes: {\n                    \"ui-autocomplete\": 'o_website_ui_autocomplete'\n                },\n                body: this.$editable[0].ownerDocument.body,\n                urlChosen: this._onAutocompleteClose.bind(this),\n                isDestroyed: () => status(this) === 'destroyed',\n            };\n            const unmountAutocompleteWithPages = wUtils.autocompleteWithPages(input, options);\n            return () => unmountAutocompleteWithPages();\n            }, () => [this.linkComponentWrapperRef.el]);\n    },\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptPageAnchor() {\n        const urlInputValue = this.$el.find('input[name=\"url\"]').val();\n        const $pageAnchor = this.$el.find('.o_link_dialog_page_anchor');\n        const showAnchorSelector = (urlInputValue[0] === '/') && (!urlInputValue.startsWith(\"/web/content/\"));\n        const $selectMenu = this.$el.find('we-selection-items[name=\"link_anchor\"]');\n\n        if ($selectMenu.data(\"anchor-for\") !== urlInputValue) { // avoid useless query\n            $pageAnchor.toggleClass('d-none', !showAnchorSelector);\n            $selectMenu.empty();\n            if (showAnchorSelector) {\n                const always = () => {\n                    const anchor = `#${urlInputValue.split('#')[1]}`;\n                    let weTogglerText = '\\u00A0';\n                    if (anchor) {\n                        const weButtonEls = $selectMenu[0].querySelectorAll('we-button');\n                        if (Array.from(weButtonEls).some(el => el.textContent === anchor)) {\n                            weTogglerText = anchor;\n                        }\n                    }\n                    $pageAnchor[0].querySelector('we-toggler').textContent = weTogglerText;\n                };\n                const urlWithoutHash = urlInputValue.split(\"#\")[0];\n                wUtils.loadAnchors(urlWithoutHash, this.$editable[0].ownerDocument.body).then(anchors => {\n                    for (const anchor of anchors) {\n                        const $option = $('<we-button class=\"dropdown-item\">');\n                        $option.text(anchor);\n                        $option.data('value', anchor);\n                        $selectMenu.append($option);\n                    }\n                }).finally(always);\n            }\n        }\n        $selectMenu.data(\"anchor-for\", urlInputValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAutocompleteClose() {\n        this.__onURLInput();\n    },\n    /**\n     * @override\n     */\n    _onURLInput() {\n        super._onURLInput(...arguments);\n        this._adaptPageAnchor();\n    },\n    /**\n     * @override\n     * @param {Event} ev\n     */\n    _onPickSelectOption(ev) {\n        if (ev.currentTarget.closest('[name=\"link_anchor\"]')) {\n            const anchorValue = $(ev.currentTarget).data('value');\n            const $urlInput = this.$el.find('[name=\"url\"]');\n            let urlInputValue = $urlInput.val();\n            if (urlInputValue.indexOf('#') > -1) {\n                urlInputValue = urlInputValue.substr(0, urlInputValue.indexOf('#'));\n            }\n            $urlInput.val(urlInputValue + anchorValue);\n            // Updates the link in the DOM with the chosen anchor.\n            this.__onURLInput();\n        }\n        super._onPickSelectOption(...arguments);\n    },\n});\n\npatch(Wysiwyg.prototype, {\n    /**\n     * @override\n     */\n    _getDelayBlurSelectors() {\n        return super._getDelayBlurSelectors().concat([\".ui-autocomplete\"]);\n    },\n});\n", "import { App, Component, useState, xml } from \"@odoo/owl\";\nimport { getTemplate } from \"@web/core/templates\";\nimport { _t } from \"@web/core/l10n/translation\";\n\nconst rootTemplate = xml`<SubComp t-props=\"state\"/>`;\nexport async function attachComponent(parent, element, componentClass, props = {}) {\n    class Root extends Component {\n        static template = rootTemplate;\n        static components = { SubComp: componentClass };\n        static props = [\"*\"];\n        state = useState(props);\n    }\n\n    const env = Component.env;\n    const app = new App(Root, {\n        env,\n        getTemplate,\n        dev: env.debug,\n        translatableAttributes: [\"data-tooltip\"],\n        translateFn: _t,\n    });\n\n    if (parent.__parentedMixin) {\n        parent.__parentedChildren.push({\n            get $el() {\n                return $(app.root.el);\n            },\n            destroy() {\n                app.destroy();\n            },\n        });\n    }\n\n    const originalValidateTarget = App.validateTarget;\n    App.validateTarget = () => {};\n    const mountPromise = app.mount(element);\n    App.validateTarget = originalValidateTarget;\n    const component = await mountPromise;\n    const subComp = Object.values(component.__owl__.children)[0].component;\n    return {\n        component: subComp,\n        destroy() {\n            app.destroy();\n        },\n        update(props) {\n            Object.assign(component.state, props);\n        },\n    };\n}\n", "/** @odoo-module **/\n\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { useService, useBus } from \"@web/core/utils/hooks\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { useDragAndDrop } from \"@web_editor/js/editor/drag_and_drop\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport * as gridUtils from \"@web_editor/js/common/grid_layout_utils\";\nimport { escape } from \"@web/core/utils/strings\";\nimport { closestElement, isUnremovable } from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\nimport { debounce, throttleForAnimation } from \"@web/core/utils/timing\";\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport { sortBy, unique } from \"@web/core/utils/arrays\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { Toolbar } from \"@web_editor/js/editor/toolbar\";\nimport {\n    Component,\n    EventBus,\n    markup,\n    onMounted,\n    onWillStart,\n    onWillUnmount,\n    useEffect,\n    useRef,\n    useState,\n} from \"@odoo/owl\";\nimport { LinkTools } from '@web_editor/js/wysiwyg/widgets/link_tools';\nimport { touching, closest, addLoadingEffect as addButtonLoadingEffect } from \"@web/core/utils/ui\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { RPCError } from \"@web/core/network/rpc\";\nimport { ColumnLayoutMixin } from \"@web_editor/js/common/column_layout_mixin\";\nimport { Tooltip as OdooTooltip } from \"@web/core/tooltip/tooltip\";\nimport { AddSnippetDialog } from \"@web_editor/js/editor/add_snippet_dialog\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\n\nlet cacheSnippetTemplate = {};\n\nvar globalSelector = {\n    closest: () => $(),\n    all: () => $(),\n    is: () => false,\n};\n\n/**\n * Management of the overlay and option list for a snippet.\n */\nvar SnippetEditor = publicWidget.Widget.extend({\n    template: 'web_editor.snippet_overlay',\n    events: {\n        'click .oe_snippet_remove': '_onRemoveClick',\n        'wheel': '_onMouseWheel',\n        'click .o_send_back': '_onSendBackClick',\n        'click .o_bring_front': '_onBringFrontClick',\n        'click .o_snippet_replace': '_onReplaceClick',\n    },\n    custom_events: {\n        'option_update': '_onOptionUpdate',\n        'user_value_widget_request': '_onUserValueWidgetRequest',\n        'snippet_option_visibility_update': '_onSnippetOptionVisibilityUpdate',\n    },\n    layoutElementsSelector: [\n        '.o_we_shape',\n        '.o_we_bg_filter',\n    ].join(','),\n\n    /**\n     * @constructor\n     * @param {PublicWidget} parent\n     * @param {Element} target\n     * @param {Object} templateOptions\n     * @param {jQuery} $editable\n     * @param {Object} options\n     */\n    init: function (parent, target, templateOptions, $editable, options) {\n        this._super.apply(this, arguments);\n        this.options = options;\n        // This is possible to have a snippet editor not inside an editable area\n        // (data-no-check=\"true\") and it is possible to not have editable areas\n        // at all (restricted editor), in that case we just suppose this is the\n        // body so related code can still be executed without crash (as we still\n        // need to instantiate instances of editors even if nothing is really\n        // editable (data-no-check=\"true\" / navigation options / ...)).\n        // TODO this should probably be reviewed in master: do we need a\n        // reference to the editable area? There should be workarounds.\n        this.$editable = $editable && $editable.length ? $editable : $(document.body);\n        this.ownerDocument = this.$editable[0].ownerDocument;\n        this.$body = $(this.ownerDocument.body);\n        this.$target = $(target);\n        this.$target.data('snippet-editor', this);\n        this.templateOptions = templateOptions;\n        this.isTargetParentEditable = false;\n        this.isTargetMovable = false;\n        this.$scrollingElement = $().getScrollingElement(this.$editable[0].ownerDocument);\n        if (!this.$scrollingElement[0]) {\n            this.$scrollingElement = $(this.ownerDocument).find('.o_editable');\n        }\n        this.displayOverlayOptions = false;\n        this._$toolbarContainer = $();\n\n        this.__isStarted = new Promise(resolve => {\n            this.__isStartedResolveFunc = resolve;\n        });\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var defs = [this._super.apply(this, arguments)];\n\n        // Initialize the associated options (see snippets.options.js)\n        defs.push(this._initializeOptions());\n        var $customize = this._customize$Elements[this._customize$Elements.length - 1];\n\n        this.isTargetParentEditable = this.$target.parent().is(':o_editable');\n        this.isTargetMovable = this.isTargetParentEditable && this.isTargetMovable && !this.$target.hasClass('oe_unmovable');\n        this.isTargetRemovable = this.isTargetParentEditable && !this.$target.parent().is('[data-oe-type=\"image\"]') && !isUnremovable(this.$target[0]);\n        this.displayOverlayOptions = this.displayOverlayOptions || this.isTargetMovable || !this.isTargetParentEditable;\n\n        // Initialize move/clone/remove buttons\n        if (this.isTargetMovable) {\n            this.dropped = false;\n            this.draggableComponent = this._initDragAndDrop(\".o_move_handle\", \".oe_overlay\", this.el);\n            if (!this.$target[0].matches(\"section\")) {\n                // Allow the user to drag the image itself to move the target.\n                // Note that the o_draggable class will be added by the\n                // _initDragAndDrop function. So adding it here is probably\n                // useless. To check. The fact that that class is added in any\n                // case should probably reviewed in master anyway (TODO).\n                this.$target[0].classList.add(\"o_draggable\");\n                this.draggableComponentImgs = this._initDragAndDrop(\"img\", \".o_draggable\", this.$target[0]);\n            }\n        } else {\n            this.$('.o_overlay_move_options').addClass('d-none');\n            const cloneButtonEl = $customize[0].querySelector(\".oe_snippet_clone\");\n            cloneButtonEl.classList.toggle(\"d-none\", !this.forceDuplicateButton);\n        }\n\n        if (!this.isTargetRemovable) {\n            this.$el.add($customize).find('.oe_snippet_remove').addClass('d-none');\n        }\n\n        // Snippets are replaceable only if they are not within another snippet.\n        // (e.g., a \"s_countdown\" is not replaceable when it is dropped as inner\n        // content)\n        if (this.$target[0].matches(\"[data-snippet]:not([data-snippet] *), .oe_structure > *\")\n                && !this.$target[0].matches(\".oe_structure_solo *\")) {\n            this.trigger_up('find_snippet_template', {\n                snippet: this.$target[0],\n                callback: (snippet) => {\n                    if (snippet.group) {\n                        this.$el.add($customize).find('.o_snippet_replace').removeClass('d-none');\n                    }\n                },\n            });\n        }\n\n        var _animationsCount = 0;\n        this.postAnimationCover = throttleForAnimation(() => {\n            this.trigger_up('cover_update', {\n                overlayVisible: true,\n            });\n        });\n        this.$target.on('transitionstart.snippet_editor, animationstart.snippet_editor', () => {\n            // We cannot rely on the fact each transition/animation start will\n            // trigger a transition/animation end as the element may be removed\n            // from the DOM before or it could simply be an infinite animation.\n            //\n            // By simplicity, for each start, we add a delayed operation that\n            // will decrease the animation counter after a fixed duration and\n            // do the post animation cover if none is registered anymore.\n            _animationsCount++;\n            setTimeout(() => {\n                if (!--_animationsCount) {\n                    this.postAnimationCover();\n                }\n            }, 500); // This delay have to be huge enough to take care of long\n                     // animations which will not trigger an animation end event\n                     // but if it is too small for some, this is the job of the\n                     // animation creator to manually ask for a re-cover\n        });\n        // On top of what is explained above, do the post animation cover for\n        // each detected transition/animation end so that the user does not see\n        // a flickering when not needed.\n        this.$target.on('transitionend.snippet_editor, animationend.snippet_editor', this.postAnimationCover);\n\n        return Promise.all(defs).then(() => {\n            this.__isStartedResolveFunc(this);\n        });\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        // Before actually destroying a snippet editor, notify the parent\n        // about it so that it can update its list of alived snippet editors.\n        this.trigger_up('snippet_editor_destroyed');\n        this.draggableComponent && this.draggableComponent.destroy();\n        this.draggableComponentImgs?.destroy();\n        if (this.$optionsSection) {\n            this.$optionsSection.remove();\n        }\n        if (this.postAnimationCover) {\n            this.postAnimationCover.cancel();\n        }\n        this._super(...arguments);\n        this.$target.removeData('snippet-editor');\n        this.$target.off('.snippet_editor');\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Checks whether the snippet options are shown or not.\n     *\n     * @returns {boolean}\n     */\n    areOptionsShown: function () {\n        const lastIndex = this._customize$Elements.length - 1;\n        return !!this._customize$Elements[lastIndex].parent().length;\n    },\n    /**\n     * Notifies all the associated snippet options that the snippet has just\n     * been dropped in the page.\n     *\n     * @param {HTMLElement} targetEl the snippet dropped in the page\n     */\n    async buildSnippet(targetEl) {\n        for (var i in this.styles) {\n            await this.styles[i].onBuilt({\n                isCurrent: targetEl === this.$target[0],\n            });\n        }\n        await this.toggleTargetVisibility(true, true);\n    },\n    /**\n     * Notifies all the associated snippet options that the template which\n     * contains the snippet is about to be saved.\n     */\n    cleanForSave: async function () {\n        if (this.isDestroyed()) {\n            return;\n        }\n        await this.toggleTargetVisibility(!this.$target.hasClass('o_snippet_invisible')\n            && !this.$target.hasClass('o_snippet_mobile_invisible')\n            && !this.$target.hasClass('o_snippet_desktop_invisible'));\n        const proms = Object.values(this.styles).map((option) => {\n            return option.cleanForSave();\n        });\n        await Promise.all(proms);\n        await this.cleanUI();\n    },\n    /**\n     * Notifies all the associated snippet options that the snippet UI needs to\n     * be cleaned.\n     */\n    async cleanUI() {\n        const proms = Object.values(this.styles).map((option) => {\n            return option.cleanUI();\n        });\n        await Promise.all(proms);\n    },\n    /**\n     * Closes all widgets of all options.\n     */\n    closeWidgets: function () {\n        if (!this.styles || !this.areOptionsShown()) {\n            return;\n        }\n        Object.keys(this.styles).forEach(key => {\n            this.styles[key].closeWidgets();\n        });\n    },\n    /**\n     * Makes the editor overlay cover the associated snippet.\n     */\n    cover: function () {\n        if (!this.isShown() || !this.$target.length) {\n            return;\n        }\n\n        const $modal = this.$target.find('.modal:visible');\n        const $target = $modal.length ? $modal : this.$target;\n        const targetEl = $target[0];\n\n        // Check first if the target is still visible, otherwise we have to\n        // hide it. When covering all element after scroll for instance it may\n        // have been hidden (part of an affixed header for example) or it may\n        // be outside of the viewport (the whole header during an effect for\n        // example).\n        const rect = targetEl.getBoundingClientRect();\n        // TODO: At that point, targetEl.ownerDocument.defaultView should not be\n        // null. However, there is a non-deterministic race condition that can\n        // result in the document being unloaded from the iframe before the handlers\n        // of the snippets menu are removed, thus triggering a traceback if the\n        // optional chaining operator is removed. This can be reproduced\n        // non-deterministically on runbot by running the edit_menus tour.\n        const vpWidth = targetEl.ownerDocument.defaultView?.innerWidth || document.documentElement.clientWidth;\n        const vpHeight = targetEl.ownerDocument.defaultView?.innerHeight || document.documentElement.clientHeight;\n        const isInViewport = (\n            rect.bottom > -0.1 &&\n            rect.right > -0.1 &&\n            (vpHeight - rect.top) > -0.1 &&\n            (vpWidth - rect.left) > -0.1\n        );\n        const hasSize = ( // :visible not enough for images\n            Math.abs(rect.bottom - rect.top) > 0.01 &&\n            Math.abs(rect.right - rect.left) > 0.01\n        );\n        if (!isInViewport || !hasSize || !this.$target.is(`:visible`)) {\n            this.toggleOverlayVisibility(false);\n            return;\n        }\n\n        const transform = window.getComputedStyle(targetEl).getPropertyValue('transform');\n        const transformOrigin = window.getComputedStyle(targetEl).getPropertyValue('transform-origin');\n        targetEl.classList.add('o_transform_removal');\n\n        // Now cover the element\n        const offset = $target.offset();\n\n        // The manipulator is supposed to follow the scroll of the content\n        // naturally without any JS recomputation.\n        const manipulatorOffset = this.$el.parent().offset();\n        offset.top -= manipulatorOffset.top;\n        offset.left -= manipulatorOffset.left;\n        this.$el.css({\n            width: $target.outerWidth(),\n            height: $target.outerHeight(),\n            left: offset.left,\n            top: offset.top,\n            transform,\n            'transform-origin': transformOrigin,\n        });\n        this.$('.o_handles').css('height', $target.outerHeight());\n\n        targetEl.classList.remove('o_transform_removal');\n\n        const editableOffsetTop = this.$editable.offset().top - manipulatorOffset.top;\n        this.$el.toggleClass('o_top_cover', offset.top - editableOffsetTop < 25);\n        // If the element covered by the overlay has a scrollbar, we remove its\n        // right border as it interferes with proper scrolling. (e.g. modal)\n        const handleEReadonlyEl = this.$el[0].querySelector('.o_handle.e.readonly');\n        if (handleEReadonlyEl) {\n            handleEReadonlyEl.style.width = $(targetEl).hasScrollableContent() ? 0 : '';\n        }\n    },\n    /**\n     * DOMElements have a default name which appears in the overlay when they\n     * are being edited. This method retrieves this name; it can be defined\n     * directly in the DOM thanks to the `data-name` attribute.\n     */\n    getName: function () {\n        if (this.$target.data('name') !== undefined) {\n            return this.$target.data('name');\n        }\n        if (this.$target.is('img')) {\n            return _t(\"Image\");\n        }\n        if (this.$target.is('.fa')) {\n            return _t(\"Icon\");\n        }\n        if (this.$target.is('.media_iframe_video')) {\n            return _t(\"Video\");\n        }\n        if (this.$target.parent('.row').length) {\n            return _t(\"Column\");\n        }\n        if (this.$target.is('#wrapwrap > main')) {\n            return _t(\"Page Options\");\n        }\n        if (this.$target[0].matches(\".btn\")) {\n            return _t(\"Button\");\n        }\n        return _t(\"Block\");\n    },\n    /**\n     * @return {boolean}\n     */\n    isShown: function () {\n        return this.$el && this.$el.parent().length && this.$el.hasClass('oe_active');\n    },\n    /**\n     * @returns {boolean}\n     */\n    isSticky: function () {\n        return this.$el && this.$el.hasClass('o_we_overlay_sticky');\n    },\n    /**\n     * @returns {boolean}\n     */\n    isTargetVisible: function () {\n        return (this.$target[0].dataset.invisible !== '1');\n    },\n    /**\n     * Removes the associated snippet from the DOM and destroys the associated\n     * editor (itself).\n     *\n     * @param {boolean} [shouldRecordUndo=true]\n     * @returns {Promise}\n     */\n    removeSnippet: async function (shouldRecordUndo = true) {\n        this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n        this.toggleOverlay(false);\n        await this.toggleOptions(false);\n        // If it is an invisible element, we must close it before deleting it\n        // (e.g. modal).\n        await this.toggleTargetVisibility(!this.$target.hasClass('o_snippet_invisible'));\n        this.trigger_up('will_remove_snippet', {$target: this.$target});\n\n        // Call the onRemove of all internal options\n        await new Promise(resolve => {\n            this.trigger_up('call_for_each_child_snippet', {\n                $snippet: this.$target,\n                callback: async function (editor, $snippet) {\n                    for (var i in editor.styles) {\n                        await editor.styles[i].onRemove();\n                    }\n                },\n                onSuccess: resolve,\n            });\n        });\n\n        // TODO this should probably be awaited but this is not possible right\n        // now as removeSnippet can be called in a locked editor mutex context\n        // and would thus produce a deadlock. Also, this awaited\n        // 'activate_snippet' call would allow to remove the 'toggleOverlay' and\n        // 'toggleOptions' calls at the start of this function.\n        // TODO also to be checked: this not being awaited, the DOM is removed\n        // first, destroying the related editors and not calling onBlur... to\n        // check if this has always been like this or not and this should be\n        // unit tested.\n        let parent = this.$target[0].parentElement;\n        let nextSibling = this.$target[0].nextElementSibling;\n        while (nextSibling && nextSibling.matches('.o_snippet_invisible')) {\n            nextSibling = nextSibling.nextElementSibling;\n        }\n        let previousSibling = this.$target[0].previousElementSibling;\n        while (previousSibling && previousSibling.matches('.o_snippet_invisible')) {\n            previousSibling = previousSibling.previousElementSibling;\n        }\n        if ($(parent).is('.o_editable:not(body)')) {\n            // If we target the editable, we want to reset the selection to the\n            // body. If the editable has options, we do not want to show them.\n            parent = $(parent).closest('body');\n        }\n        const activateSnippetProm = new Promise(resolve => {\n            this.trigger_up('activate_snippet', {\n                $snippet: $(previousSibling || nextSibling || parent),\n                onSuccess: resolve,\n            });\n        });\n\n        // Actually remove the snippet and its option UI.\n        var $parent = this.$target.parent();\n        this.$target.find('*').addBack().each((index, el) => {\n            const tooltip = Tooltip.getInstance(el);\n            if (tooltip) {\n                tooltip.dispose();\n            }\n        });\n        this.$target.remove();\n        this.$el.remove();\n\n        // Resize the grid to have the correct row count.\n        // Must be done here and not in a dedicated onRemove method because\n        // onRemove is called before actually removing the element and it\n        // should be the case in order to resize the grid.\n        if (this.$target[0].classList.contains('o_grid_item')) {\n            gridUtils._resizeGrid($parent[0]);\n        }\n\n        var node = $parent[0];\n        if (node && node.firstChild) {\n            if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {\n                node.removeChild(node.firstChild);\n            }\n        }\n\n        // Potentially remove ancestors (like when removing the last column of a\n        // snippet).\n        if ($parent.closest(':data(\"snippet-editor\")').length) {\n            const isEmptyAndRemovable = ($el, editor) => {\n                editor = editor || $el.data('snippet-editor');\n\n                // Consider a <figure> element as empty if it only contains a\n                // <figcaption> element (e.g., when its image has just been\n                // removed).\n                const isEmptyFigureEl = $el[0].matches(\"figure\")\n                    && $el[0].children.length === 1\n                    && $el[0].children[0].matches(\"figcaption\");\n\n                const isEmpty = isEmptyFigureEl || ($el.text().trim() === ''\n                    && $el.children().toArray().every(el => {\n                        // Consider layout-only elements (like bg-shapes) as empty\n                        return el.matches(this.layoutElementsSelector);\n                    }));\n                const notRemovableSelector =\n                    `.oe_structure,\n                    .carousel-item,\n                    .carousel-item > .container,\n                    .carousel-item > .container-fluid,\n                    .carousel-item > .o_container_small`;\n                return isEmpty && !$el[0].matches(notRemovableSelector)\n                    && (!editor || editor.isTargetParentEditable)\n                    && !isUnremovable($el[0]);\n            };\n\n            var editor = $parent.data('snippet-editor');\n            while (!editor) {\n                var $nextParent = $parent.parent();\n                if (isEmptyAndRemovable($parent)) {\n                    $parent.remove();\n                }\n                $parent = $nextParent;\n                editor = $parent.data('snippet-editor');\n            }\n            if (isEmptyAndRemovable($parent, editor)) {\n                // TODO maybe this should be part of the actual Promise being\n                // returned by the function ?\n                setTimeout(() => editor.removeSnippet());\n            }\n        }\n\n        // Clean editor if they are image or table in deleted content\n        this.$body.find('.note-control-selection').hide();\n        this.$body.find('.o_table_handler').remove();\n\n        this.trigger_up('snippet_removed');\n        // FIXME that whole Promise should be awaited before the DOM removal etc\n        // as explained above where it is defined. However, it is critical to at\n        // least await it before destroying the snippet editor instance\n        // otherwise the logic of activateSnippet gets messed up.\n        // FIXME should not this call _destroyEditor ?\n        activateSnippetProm.then(() => this.destroy());\n        $parent.trigger('content_changed');\n\n        // TODO Page content changed, some elements may need to be adapted\n        // according to it. While waiting for a better way to handle that this\n        // window trigger will handle most cases.\n        $(window).trigger('resize');\n\n        if (shouldRecordUndo) {\n            this.options.wysiwyg.odooEditor.historyStep();\n        }\n    },\n    /**\n     * Displays/Hides the editor overlay.\n     *\n     * @param {boolean} show\n     * @param {boolean} [previewMode=false]\n     */\n    toggleOverlay: function (show, previewMode) {\n        if (!this.$el) {\n            return;\n        }\n\n        if (previewMode) {\n            // In preview mode, the sticky classes are left untouched, we only\n            // add/remove the preview class when toggling/untoggling\n            this.$el.toggleClass('o_we_overlay_preview', show);\n        } else {\n            // In non preview mode, the preview class is always removed, and the\n            // sticky class is added/removed when toggling/untoggling\n            this.$el.removeClass('o_we_overlay_preview');\n            this.$el.toggleClass('o_we_overlay_sticky', show);\n            if (!this.displayOverlayOptions) {\n                this.$el.find('.o_overlay_options_wrap').addClass('o_we_hidden_overlay_options');\n            }\n        }\n\n        // Show/hide overlay in preview mode or not\n        this.$el.toggleClass('oe_active', show);\n        this.cover();\n        this.toggleOverlayVisibility(show);\n    },\n    /**\n     * Updates the UI of the editor (+ parent) options and call onFocus/onBlur\n     * if necessary. The UI jquery elements to display are returned, it is up\n     * to the caller to actually display them or not.\n     *\n     * @param {boolean} show\n     * @returns {Promise<jQuery[]>}\n     */\n    async toggleOptions(show) {\n        if (!this.$el) {\n            return [];\n        }\n\n        if (this.areOptionsShown() === show) {\n            return null;\n        }\n\n        // All onFocus before all ui updates as the onFocus of an option might\n        // affect another option (like updating the $target)\n        const editorUIsToUpdate = [];\n        const focusOrBlur = show\n            ? async (editor, options) => {\n                for (const opt of options) {\n                    await opt.onFocus();\n                }\n                editorUIsToUpdate.push(editor);\n            }\n            : async (editor, options) => {\n                for (const opt of options) {\n                    await opt.onBlur();\n                }\n            };\n        for (const $el of this._customize$Elements) {\n            const editor = $el.data('editor');\n            const styles = sortBy(Object.values(editor.styles || {}), \"__order\");\n            await focusOrBlur(editor, styles);\n        }\n        await Promise.all(editorUIsToUpdate.map(editor => editor.updateOptionsUI()));\n        // A `d-none` class is added to option sections that have no visible\n        // options with `updateOptionsUIVisibility`. If no option section is\n        // visible (including the options moved to the toolbar), we prevent\n        // the activation of the options.\n        const optionsSectionVisible = await Promise.all(\n            editorUIsToUpdate.map(editor => editor.updateOptionsUIVisibility())\n        ).then(editorVisibilityValues => {\n            return editorVisibilityValues.some(editorVisibilityValue => editorVisibilityValue);\n        });\n        if (editorUIsToUpdate.length > 0 && !optionsSectionVisible) {\n            return null;\n        }\n        return this._customize$Elements;\n    },\n    /**\n     * @param {boolean} [show]\n     * @param {boolean} [ignoreDeviceVisibility]\n     * @returns {Promise<boolean>}\n     */\n    toggleTargetVisibility: async function (show, ignoreDeviceVisibility) {\n        show = this._toggleVisibilityStatus(show, ignoreDeviceVisibility);\n        var styles = Object.values(this.styles);\n        const proms = sortBy(styles, \"__order\").map((style) => {\n            return show ? style.onTargetShow() : style.onTargetHide();\n        });\n        await Promise.all(proms);\n        return show;\n    },\n    /**\n     * @param {boolean} [show=false]\n     */\n    toggleOverlayVisibility: function (show) {\n        if (this.$el && !this.scrollingTimeout) {\n            this.$el.toggleClass('o_overlay_hidden', (!show || this.$target[0].matches('.o_animating:not(.o_animate_on_scroll)')) && this.isShown());\n        }\n    },\n    /**\n     * Updates the UI of all the options according to the status of their\n     * associated editable DOM. This does not take care of options *visibility*.\n     * For that @see updateOptionsUIVisibility, which should called when the UI\n     * is up-to-date thanks to the function here, as the visibility depends on\n     * the UI's status.\n     *\n     * @param {boolean} [assetsChanged=false]\n     * @returns {Promise}\n     */\n    async updateOptionsUI(assetsChanged) {\n        const proms = Object.values(this.styles).map(opt => {\n            return opt.updateUI({noVisibility: true, assetsChanged: assetsChanged});\n        });\n        return Promise.all(proms);\n    },\n    /**\n     * Updates the visibility of the UI of all the options according to the\n     * status of their associated dependencies and related editable DOM status.\n     *\n     * @returns {Promise}\n     */\n    async updateOptionsUIVisibility() {\n        const proms = Object.values(this.styles).map(opt => {\n            return opt.updateUIVisibility();\n        });\n        // Get information about the visibility of options (except the ones\n        // located in the overlay). This is needed to check if the editor has\n        // visible options outside the options section.\n        const someOptionsVisible = await Promise.all(proms).then(optionsVisibilityValues => {\n            return optionsVisibilityValues.some(optionsVisibilityValue => optionsVisibilityValue);\n        });\n        // Hide the snippetEditor if none of its options are visible\n        // This cannot be done using the visibility of the options' UI\n        // because some options can be located in the overlay / toolbar.\n        const visibleOptionsInSection = this.$optionsSection.find('we-top-button-group, we-customizeblock-option')\n            .children(':not(.d-none)').length;\n        // Some options (e.g., text highlights / animations) may have a special\n        // way to be displayed in the editor: We add the options in the toolbar\n        // `onFocus()` and set them back `onBlur()`. Which means that the\n        // options section will be empty and should be hidden, while editor's\n        // visible options should be displayed in the toolbar DOM. We need to\n        // take this scenario into consideration too.\n        const optionsSectionEmpty = !this.$optionsSection[0].querySelector(\":scope > we-customizeblock-option\");\n        const optionsSectionVisible = visibleOptionsInSection && !optionsSectionEmpty;\n        // At this level, we can hide the options section.\n        this.$optionsSection.toggleClass(\"d-none\", !optionsSectionVisible);\n        // Even with a hidden options section, the editor is still considered\n        // visible\" if it has visible toolbar options.\n        return optionsSectionVisible || someOptionsVisible;\n    },\n    /**\n     * Clones the current snippet.\n     *\n     * @param {boolean} recordUndo\n     */\n    clone: async function (recordUndo) {\n        this.trigger_up('snippet_will_be_cloned', {$target: this.$target});\n\n        await new Promise(resolve => {\n            this.trigger_up(\"clean_ui_request\", {\n                targetEl: this.$target[0],\n                onSuccess: resolve,\n            });\n        });\n\n        var $clone = this.$target.clone(false);\n\n        this.$target.after($clone);\n\n        if (recordUndo) {\n            this.options.wysiwyg.odooEditor.historyStep(true);\n        }\n        await new Promise(resolve => {\n            this.trigger_up('call_for_each_child_snippet', {\n                $snippet: $clone,\n                callback: function (editor, $snippet) {\n                    for (var i in editor.styles) {\n                        editor.styles[i].onClone({\n                            isCurrent: ($snippet.is($clone)),\n                        });\n                    }\n                },\n                onSuccess: resolve,\n            });\n        });\n        this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$target});\n\n        $clone.trigger('content_changed');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Instantiates the snippet's options.\n     *\n     * @private\n     */\n    _initializeOptions: function () {\n        this._customize$Elements = [];\n        this.styles = {};\n        this.selectorSiblings = [];\n        this.selectorChildren = [];\n        this.selectorLockWithin = new Set();\n        const selectorExcludeAncestor = new Set();\n\n        if (this.options.allowParentsEditors) {\n            // TODO Should not rely on .data('snippet-editor') but ask parents\n            var $element = this.$target.parent();\n            while ($element.length) {\n                var parentEditor = $element.data('snippet-editor');\n                if (parentEditor) {\n                    this._customize$Elements = this._customize$Elements\n                        .concat(parentEditor._customize$Elements);\n                    break;\n                }\n                $element = $element.parent();\n            }\n        }\n\n        var $optionsSection = $(renderToElement('web_editor.customize_block_options_section', {\n            name: this.getName(),\n        })).data('editor', this);\n        const $optionsSectionBtnGroup = $optionsSection.find('we-top-button-group');\n        $optionsSectionBtnGroup.contents().each((i, node) => {\n            if (node.nodeType === Node.TEXT_NODE) {\n                node.parentNode.removeChild(node);\n            }\n        });\n        this.$optionsSection = $optionsSection;\n        $optionsSection.on('mouseenter', this._onOptionsSectionMouseEnter.bind(this));\n        $optionsSection.on('mouseleave', this._onOptionsSectionMouseLeave.bind(this));\n        $optionsSection.on('click', 'we-title > span', this._onOptionsSectionClick.bind(this));\n        $optionsSection.on('click', '.oe_snippet_clone', this._onCloneClick.bind(this));\n        $optionsSection.on('click', '.oe_snippet_remove', this._onRemoveClick.bind(this));\n        this._customize$Elements.push($optionsSection);\n\n        // TODO get rid of this when possible (made as a fix to support old\n        // theme options)\n        this.$el.data('$optionsSection', $optionsSection);\n\n        var i = 0;\n        var defs = this.templateOptions.map((val) => {\n            if (!val.selector.is(this.$target)) {\n                return;\n            }\n            if (val.data.string) {\n                $optionsSection[0].querySelector('we-title > span').textContent = val.data.string;\n            }\n            if (val['drop-near']) {\n                this.selectorSiblings.push(val['drop-near']);\n            }\n            if (val['drop-in']) {\n                this.selectorChildren.push(val['drop-in']);\n            }\n            if (val['drop-lock-within']) {\n                this.selectorLockWithin.add(val['drop-lock-within']);\n            }\n            if (val['drop-exclude-ancestor']) {\n                selectorExcludeAncestor.add(val['drop-exclude-ancestor']);\n            }\n\n            var optionName = val.option;\n            var option = new (options.registry[optionName] || options.Class)(\n                this,\n                val.$el.children(),\n                val.base_target ? this.$target.find(val.base_target).eq(0) : this.$target,\n                this.$el,\n                Object.assign({\n                    optionName: optionName,\n                    snippetName: this.getName(),\n                }, val.data),\n                this.options\n            );\n            var key = optionName || uniqueId(\"option\");\n            if (this.styles[key]) {\n                // If two snippet options use the same option name (and so use\n                // the same JS option), store the subsequent ones with a unique\n                // ID (TODO improve)\n                key = uniqueId(key);\n            }\n            this.styles[key] = option;\n            option.__order = i++;\n\n            if (option.forceNoDeleteButton) {\n                this.$el.add($optionsSection).find('.oe_snippet_remove').addClass('d-none');\n                this.$el.add($optionsSection).find('.oe_snippet_clone').addClass('d-none');\n            }\n\n            if (option.displayOverlayOptions) {\n                this.displayOverlayOptions = true;\n            }\n\n            if (option.forceDuplicateButton) {\n                this.forceDuplicateButton = true;\n            }\n\n            return option.appendTo(document.createDocumentFragment());\n        });\n\n        if (selectorExcludeAncestor.size) {\n            // Prevents dropping an element into another one.\n            // (E.g. ToC inside another ToC)\n            const excludedAncestorSelector = [...selectorExcludeAncestor].join(\", \");\n            this.excludeAncestors = (i, el) => !el.closest(excludedAncestorSelector);\n        }\n\n        this.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0);\n\n        this.$el.find('[data-bs-toggle=\"dropdown\"]').dropdown();\n\n        return Promise.all(defs).then(async () => {\n            const options = sortBy(Object.values(this.styles), \"__order\");\n            const firstOptions = [];\n            options.forEach(option => {\n                if (option.isTopOption) {\n                    if (option.isTopFirstOption) {\n                        firstOptions.push(option);\n                    } else {\n                        $optionsSectionBtnGroup.prepend(option.$el);\n                    }\n                } else {\n                    $optionsSection.append(option.$el);\n                }\n            });\n            firstOptions.forEach(option => {\n                $optionsSectionBtnGroup.prepend(option.$el);\n            });\n            $optionsSection.toggleClass('d-none', options.length === 0);\n        });\n    },\n    /**\n     * Initialize drag and drop handlers.\n     *\n     * @private\n     * @param {String} handle css selector for grabble element\n     * @param {String} elementsSelector selector for elements that will be dragged.\n     * @param {HTMLElement} element element to listen for drag events.\n     * @returns {Object} the drag state.\n     */\n    _initDragAndDrop(handle, elementsSelector, element) {\n        const modalAncestorEl = this.$target[0].closest('.modal');\n        const $scrollable = modalAncestorEl && $(modalAncestorEl)\n            || (this.options.$scrollable)\n            || (this.$scrollingElement.length && this.$scrollingElement)\n            || $().getScrollingElement(this.ownerDocument);\n        const dragAndDropOptions = {\n            ref: { el: element },\n            elements: elementsSelector,\n            handle: handle,\n            scrollingElement: $scrollable[0],\n            enable: () => !!this.$el.find('.o_move_handle:visible').length || this.dragStarted,\n            helper: () => {\n                const cloneEl = this.$el[0].cloneNode(true);\n                cloneEl.style.width = \"24px\";\n                cloneEl.style.height = \"24px\";\n                cloneEl.style.border = \"0\";\n                this.$el[0].ownerDocument.body.appendChild(cloneEl);\n                cloneEl.classList.remove(\"d-none\");\n                cloneEl.classList.remove(\"o_dragged\");\n                return cloneEl;\n            },\n            onDragStart: (args) => {\n                this.dragStarted = true;\n                const targetRect = this.$target[0].getBoundingClientRect();\n                // Bound the Y mouse position to the element height minus one\n                // grid row, to be able to drag from the bottom in a grid.\n                const gridRowSize = gridUtils.rowSize;\n                const boundedYMousePosition = Math.min(args.y, targetRect.bottom - gridRowSize);\n                this.mousePositionYOnElement = boundedYMousePosition - targetRect.y;\n                this.mousePositionXOnElement = args.x - targetRect.x;\n                this._onDragAndDropStart(args);\n            },\n            onDragEnd: (...args) => {\n                if (!this.dragStarted) {\n                    return false;\n                }\n                this.dragStarted = false;\n                // Delay our stop handler so that some wysiwyg handlers\n                // which occur on mouseup (and are themself delayed) are\n                // executed first (this prevents the library to crash\n                // because our stop handler may change the DOM).\n                setTimeout(() => {\n                    this._onDragAndDropStop(...args);\n                }, 0);\n            },\n            onDrag: this._onDragMove.bind(this),\n            dropzoneOver: this.dropzoneOver.bind(this),\n            dropzoneOut: this.dropzoneOut.bind(this),\n            dropzones: () => this.$dropZones?.toArray() || [],\n        };\n        const finalOptions = this.options.getDragAndDropOptions(dragAndDropOptions);\n        return useDragAndDrop(finalOptions);\n    },\n    /**\n     * @private\n     * @param {boolean} [show]\n     * @param {boolean} [ignoreDeviceVisibility]\n     * @returns {boolean}\n     */\n    _toggleVisibilityStatus: function (show, ignoreDeviceVisibility) {\n        if (ignoreDeviceVisibility) {\n            if (this.$target[0].matches(\".o_snippet_mobile_invisible, .o_snippet_desktop_invisible\")) {\n                const isMobilePreview = weUtils.isMobileView(this.$target[0]);\n                const isMobileHidden = this.$target[0].classList.contains(\"o_snippet_mobile_invisible\");\n                if (isMobilePreview === isMobileHidden) {\n                    // Preview mode and hidden type are the same.\n                    show = false;\n                }\n            }\n        }\n        if (show === undefined) {\n            show = !this.isTargetVisible();\n        }\n        if (show) {\n            delete this.$target[0].dataset.invisible;\n        } else {\n            this.$target[0].dataset.invisible = '1';\n        }\n        return show;\n    },\n    /**\n     * Returns false if the element matches a snippet block that cannot be\n     * dropped in a sanitized HTML field or a string representing a specific\n     * reason. Returns true if no such issue exists.\n     *\n     * @param {Element} el\n     * @return {boolean|str} str indicates a specific type of forbidden sanitization\n     */\n    _canBeSanitizedUnless(el) {\n        let result = true;\n        for (const snippetEl of [el, ...el.querySelectorAll('[data-snippet]')]) {\n            this.trigger_up('find_snippet_template', {\n                snippet: snippetEl,\n                callback: function (snippet) {\n                    const forbidSanitize = snippet.data.oeForbidSanitize;\n                    if (forbidSanitize) {\n                        result = forbidSanitize === 'form' ? 'form' : false;\n                    }\n                },\n            });\n            // If some element in the block is already fully non-sanitizable,\n            // the whole block cannot be sanitized.\n            if (!result) {\n                break;\n            }\n        }\n        return result;\n    },\n    /**\n     * Called when an \"over\" dropzone event happens after an other \"over\"\n     * without an \"out\" between them. It escapes the previous dropzone.\n     *\n     * @private\n     * @param {Object} self\n     *      the same `self` variable as when we are in `_onDragAndDropStart`\n     * @param {Element} currentDropzoneEl\n     *      the dropzone over which we are currently dragging\n     */\n    _outPreviousDropzone(self, currentDropzoneEl) {\n        const previousDropzoneEl = this;\n        const rowEl = previousDropzoneEl.parentNode;\n\n        if (rowEl.classList.contains('o_grid_mode')) {\n            self.dragState.gridMode = false;\n            const fromGridToGrid = currentDropzoneEl.classList.contains('oe_grid_zone');\n            if (fromGridToGrid) {\n                // If we went from a grid dropzone to an other grid one.\n                rowEl.style.removeProperty('position');\n            } else {\n                // If we went from a grid dropzone to a normal one.\n                gridUtils._gridCleanUp(rowEl, self.$target[0]);\n                self.$target[0].style.removeProperty('z-index');\n            }\n\n            // Removing the drag helper and the background grid and\n            // resizing the grid and the dropzone.\n            self.dragState.dragHelperEl.remove();\n            self.dragState.backgroundGridEl.remove();\n            self.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n            gridUtils._resizeGrid(rowEl);\n            self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n            const rowCount = parseInt(rowEl.dataset.rowCount);\n            previousDropzoneEl.style.gridRowEnd = Math.max(rowCount + 1, 1);\n        }\n        previousDropzoneEl.classList.remove('invisible');\n    },\n    /**\n     * Changes some behaviors before the drag and drop.\n     *\n     * @private\n     * @returns {Function} a function that restores what was changed when the\n     *  drag and drop is over.\n     */\n    _prepareDrag() {\n        return () => {};\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the 'clone' button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onCloneClick: function (ev) {\n        ev.preventDefault();\n        this.clone(true);\n    },\n    /**\n     * Called when the snippet is starting to be dragged thanks to the 'move'\n     * button.\n     *\n     * @private\n     */\n    _onDragAndDropStart({ helper, addStyle }) {\n        this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n        this.trigger_up('drag_and_drop_start');\n        this.options.wysiwyg.odooEditor.automaticStepUnactive();\n        var self = this;\n        this.dragState = {};\n        const rowEl = this.$target[0].parentNode;\n        this.dragState.overFirstDropzone = true;\n\n        this.dragState.restore = this._prepareDrag();\n\n        // Allow the grid mode if the option is present in the right panel or\n        // if the grid mode is already activated.\n        let hasGridLayoutOption = false;\n        this.trigger_up('user_value_widget_request', {\n            name: 'grid_mode',\n            allowParentOption: true,\n            onSuccess: (widget) => {\n                // The grid option is considered as present only if the\n                // container element having it is the same as the container of\n                // the column we are dragging.\n                if (widget.$target[0] === rowEl.parentElement) {\n                    hasGridLayoutOption = true;\n                }\n            },\n        });\n        const allowGridMode = hasGridLayoutOption || rowEl.classList.contains('o_grid_mode');\n\n        // Number of grid columns and rows in the grid item (BS column).\n        if (rowEl.classList.contains('row') && this.options.isWebsite) {\n            if (allowGridMode) {\n                // Toggle grid mode if it is not already on.\n                if (!rowEl.classList.contains('o_grid_mode')) {\n                    this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                    const containerEl = rowEl.parentNode;\n                    gridUtils._toggleGridMode(containerEl);\n                    this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n                }\n\n                // Computing the moving column width and height in terms of columns\n                // and rows.\n                const columnStart = self.$target[0].style.gridColumnStart;\n                const columnEnd = self.$target[0].style.gridColumnEnd;\n                const rowStart = self.$target[0].style.gridRowStart;\n                const rowEnd = self.$target[0].style.gridRowEnd;\n\n                this.dragState.columnColCount = columnEnd - columnStart;\n                this.dragState.columnRowCount = rowEnd - rowStart;\n\n                // Storing the current grid and grid area to use them for the\n                // history.\n                this.dragState.startingGrid = rowEl;\n                this.dragState.prevGridArea = self.$target[0].style.gridArea;\n\n                this.dragState.startingZIndex = self.$target[0].style.zIndex;\n\n                // Reload the images.\n                gridUtils._reloadLazyImages(this.$target[0]);\n            } else {\n                // If the column comes from a snippet that doesn't toggle the\n                // grid mode on drag, store its width and height to use them\n                // when the column goes over a grid dropzone.\n                const isImageColumn = gridUtils._checkIfImageColumn(this.$target[0]);\n                if (isImageColumn) {\n                    // Store the image width and height if the column only\n                    // contains an image.\n                    const imageEl = this.$target[0].querySelector('img');\n                    this.dragState.columnWidth = parseFloat(imageEl.scrollWidth);\n                    this.dragState.columnHeight = parseFloat(imageEl.scrollHeight);\n                } else {\n                    this.dragState.columnWidth = parseFloat(this.$target[0].scrollWidth);\n                    this.dragState.columnHeight = parseFloat(this.$target[0].scrollHeight);\n                }\n                // Taking the column borders into account.\n                const style = window.getComputedStyle(this.$target[0]);\n                this.dragState.columnWidth += parseFloat(style.borderLeft) + parseFloat(style.borderRight);\n                this.dragState.columnHeight += parseFloat(style.borderTop) + parseFloat(style.borderBottom);\n            }\n            // Storing the starting top position of the column.\n            this.dragState.columnTop = this.$target[0].getBoundingClientRect().top;\n            this.dragState.isColumn = true;\n            // Deactivate the snippet so the overlay doesn't show.\n            this.trigger_up('deactivate_snippet', {$snippet: self.$target});\n        }\n\n        // If the target has a mobile order class, store its parent and order.\n        const targetMobileOrder = this.$target[0].style.order;\n        if (targetMobileOrder) {\n            this.dragState.startingParent = this.$target[0].parentNode;\n            this.dragState.mobileOrder = parseInt(targetMobileOrder);\n        }\n\n        const toInsertInline = window.getComputedStyle(this.$target[0]).display.includes('inline');\n\n        this.dropped = false;\n        this._dropSiblings = {\n            prev: self.$target.prev()[0],\n            next: self.$target.next()[0],\n        };\n        self.size = {\n            width: self.$target.width(),\n            height: self.$target.height()\n        };\n        const dropCloneEl = document.createElement(\"div\");\n        dropCloneEl.classList.add(\"oe_drop_clone\");\n        dropCloneEl.style.setProperty(\"display\", \"none\");\n        self.$target[0].after(dropCloneEl);\n        self.$target.detach();\n        self.$el.addClass('d-none');\n\n        var $selectorSiblings = $();\n        for (var i = 0; i < self.selectorSiblings.length; i++) {\n            let $siblings = self.selectorSiblings[i].all();\n            if (this.excludeAncestors) {\n                $siblings = $siblings.filter(this.excludeAncestors);\n            }\n            $selectorSiblings = $selectorSiblings ? $selectorSiblings.add($siblings) : $siblings;\n        }\n        var $selectorChildren;\n        for (i = 0; i < self.selectorChildren.length; i++) {\n            let $children = self.selectorChildren[i].all();\n            if (this.excludeAncestors) {\n                $children = $children.filter(this.excludeAncestors);\n            }\n            $selectorChildren = $selectorChildren ? $selectorChildren.add($children) : $children;\n        }\n        // Disallow dropping an element outside a given direct or\n        // indirect parent. (E.g. form field must remain within its own form)\n        for (const lockedParentSelector of this.selectorLockWithin) {\n            const closestLockedParentEl = dropCloneEl.closest(lockedParentSelector);\n            const filterFunc = (i, el) => el.closest(lockedParentSelector) === closestLockedParentEl;\n            if ($selectorSiblings) {\n                $selectorSiblings = $selectorSiblings.filter(filterFunc);\n            }\n            if ($selectorChildren) {\n                $selectorChildren = $selectorChildren.filter(filterFunc);\n            }\n        }\n\n        const canBeSanitizedUnless = this._canBeSanitizedUnless(this.$target[0]);\n\n        // Remove the siblings/children that would add a dropzone as direct\n        // child of a grid area and make a dedicated set out of the identified\n        // grid areas.\n        const selectorGrids = new Set();\n        const filterOutSelectorGrids = ($selectorItems, getDropzoneParent) => {\n            if (!$selectorItems) {\n                return;\n            }\n            // Looping backwards because elements are removed, so the\n            // indexes are not lost.\n            for (let i = $selectorItems.length - 1; i >= 0; i--) {\n                const el = getDropzoneParent($selectorItems[i]);\n                if (el.classList.contains('o_grid_mode')) {\n                    $selectorItems.splice(i, 1);\n                    selectorGrids.add(el);\n                }\n            }\n        };\n        filterOutSelectorGrids($selectorSiblings, el => el.parentElement);\n        filterOutSelectorGrids($selectorChildren, el => el);\n\n        this.trigger_up('activate_snippet', {$snippet: this.$target.parent()});\n        this.trigger_up('activate_insertion_zones', {\n            $selectorSiblings: $selectorSiblings,\n            $selectorChildren: $selectorChildren,\n            canBeSanitizedUnless: canBeSanitizedUnless,\n            toInsertInline: toInsertInline,\n            selectorGrids: selectorGrids,\n            fromIframe: true,\n        });\n\n        this.$body.addClass('move-important');\n\n        this.$dropZones = this.$editable.find('.oe_drop_zone');\n        if (!canBeSanitizedUnless) {\n            this.$dropZones = this.$dropZones.not('[data-oe-sanitize] .oe_drop_zone');\n        } else if (canBeSanitizedUnless === 'form') {\n            this.$dropZones = this.$dropZones.not('[data-oe-sanitize][data-oe-sanitize!=\"allow_form\"] .oe_drop_zone');\n        }\n    },\n    dropzoneOver({ dropzone }) {\n        if (this.dropped) {\n            this.$target.detach();\n        }\n\n        // Prevent a column to be trapped in an upper grid dropzone at\n        // the start of the drag.\n        if (this.dragState.isColumn && this.dragState.overFirstDropzone) {\n            this.dragState.overFirstDropzone = false;\n\n            // The column is considered as glued to the dropzone if the\n            // dropzone is above and if the space between them is less\n            // than 25px (the move handle height is 22px so 25 is a\n            // safety margin).\n            const columnTop = this.dragState.columnTop;\n            const dropzoneBottom = dropzone.el.getBoundingClientRect().bottom;\n            const areDropzonesGlued = (columnTop >= dropzoneBottom) && (columnTop - dropzoneBottom < 25);\n\n            if (areDropzonesGlued && dropzone.el.classList.contains('oe_grid_zone')) {\n                return;\n            }\n        }\n\n        this.dropped = true;\n        const $dropzone = $(dropzone.el).first().after(this.$target);\n        $dropzone.addClass('invisible');\n\n        // Checking if the \"out\" event happened before dropzone.el \"over\": if\n        // `this.dragState.currentDropzoneEl` exists, \"out\" didn't\n        // happen because it deletes it. We are therefore in the case\n        // of an \"over\" after an \"over\" and we need to escape the\n        // previous dropzone first.\n        if (this.dragState.currentDropzoneEl) {\n            this._outPreviousDropzone.apply(this.dragState.currentDropzoneEl, [this, $dropzone[0]]);\n        }\n        this.dragState.currentDropzoneEl = $dropzone[0];\n\n        if ($dropzone[0].classList.contains('oe_grid_zone')) {\n            // Case where the column we are dragging is over a grid\n            // dropzone.\n            const rowEl = $dropzone[0].parentNode;\n\n            // If the column doesn't come from a grid mode snippet.\n            if (!this.$target[0].classList.contains('o_grid_item')) {\n                // Converting the column to grid.\n                this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                const spans = gridUtils._convertColumnToGrid(rowEl, this.$target[0], this.dragState.columnWidth, this.dragState.columnHeight);\n                this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n                this.dragState.columnColCount = spans.columnColCount;\n                this.dragState.columnRowCount = spans.columnRowCount;\n\n                // Storing the column spans.\n            }\n\n            const columnColCount = this.dragState.columnColCount;\n            const columnRowCount = this.dragState.columnRowCount;\n            // Creating the drag helper.\n            const dragHelperEl = document.createElement('div');\n            dragHelperEl.classList.add('o_we_drag_helper');\n            dragHelperEl.style.gridArea = `1 / 1 / ${1 + columnRowCount} / ${1 + columnColCount}`;\n            rowEl.append(dragHelperEl);\n\n            // Creating the background grid and updating the dropzone\n            // (in the case where the column over the dropzone is\n            // bigger than the grid).\n            const backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, columnRowCount);\n            const rowCount = Math.max(rowEl.dataset.rowCount, columnRowCount);\n            $dropzone[0].style.gridRowEnd = rowCount + 1;\n\n            this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n            // Setting the moving grid item, the background grid and\n            // the drag helper z-indexes. The grid item z-index is set\n            // to its original one if we are in its starting grid, or\n            // to the maximum z-index of the grid otherwise.\n            if (rowEl === this.dragState.startingGrid) {\n                this.$target[0].style.zIndex = this.dragState.startingZIndex;\n            } else {\n                gridUtils._setElementToMaxZindex(this.$target[0], rowEl);\n            }\n            gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl);\n            gridUtils._setElementToMaxZindex(dragHelperEl, rowEl);\n\n            // Setting the column height and width to keep its size\n            // when the grid-area is removed (as it prevents it from\n            // moving with the mouse).\n            const gridProp = gridUtils._getGridProperties(rowEl);\n            const columnHeight = columnRowCount * (gridProp.rowSize + gridProp.rowGap) - gridProp.rowGap;\n            const columnWidth = columnColCount * (gridProp.columnSize + gridProp.columnGap) - gridProp.columnGap;\n            this.$target[0].style.height = columnHeight + 'px';\n            this.$target[0].style.width = columnWidth + 'px';\n            this.$target[0].style.position = 'absolute';\n            this.$target[0].style.removeProperty('grid-area');\n            rowEl.style.position = 'relative';\n            this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n\n            // Storing useful information and adding an event listener.\n            this.dragState.startingHeight = rowEl.clientHeight;\n            this.dragState.currentHeight = rowEl.clientHeight;\n            this.dragState.dragHelperEl = dragHelperEl;\n            this.dragState.backgroundGridEl = backgroundGridEl;\n            this.dragState.gridMode = true;\n        }\n    },\n    dropzoneOut({ dropzone }) {\n        const rowEl = dropzone.el.parentNode;\n\n        // Checking if the \"out\" event happens right after the \"over\"\n        // of the same dropzone. If it is not the case, we don't do\n        // anything since the previous dropzone was already escaped (at\n        // the start of the over).\n        const sameDropzoneAsCurrent = this.dragState.currentDropzoneEl === dropzone.el;\n\n        if (sameDropzoneAsCurrent) {\n            if (rowEl.classList.contains('o_grid_mode')) {\n                // Removing the listener + cleaning.\n                this.dragState.gridMode = false;\n                gridUtils._gridCleanUp(rowEl, this.$target[0]);\n                this.$target[0].style.removeProperty('z-index');\n\n                // Removing the drag helper and the background grid and\n                // resizing the grid and the dropzone.\n                this.dragState.dragHelperEl.remove();\n                this.dragState.backgroundGridEl.remove();\n                this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                gridUtils._resizeGrid(rowEl);\n                this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n                const rowCount = parseInt(rowEl.dataset.rowCount);\n                dropzone.el.style.gridRowEnd = Math.max(rowCount + 1, 1);\n            }\n\n            var prev = this.$target.prev();\n            if (dropzone.el === prev[0]) {\n                this.dropped = false;\n                this.$target.detach();\n                $(dropzone.el).removeClass('invisible');\n            }\n\n            delete this.dragState.currentDropzoneEl;\n        }\n    },\n    /**\n     * Called when the snippet is dropped after being dragged thanks to the\n     * 'move' button.\n     *\n     * @private\n     * @param {Event} ev\n     * @param {Object} ui\n     */\n    _onDragAndDropStop({ x, y }) {\n        this.options.wysiwyg.odooEditor.automaticStepActive();\n        this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n        this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n\n        const rowEl = this.$target[0].parentNode;\n        if (rowEl && rowEl.classList.contains('o_grid_mode')) {\n            // Case when dropping the column in a grid.\n\n            // Disable dragMove handler\n            this.dragState.gridMode = false;\n\n            // Defining the column grid area with its position.\n            const gridProp = gridUtils._getGridProperties(rowEl);\n\n            const style = window.getComputedStyle(this.$target[0]);\n            const top = parseFloat(style.top);\n            const left = parseFloat(style.left);\n\n            const rowStart = Math.round(top / (gridProp.rowSize + gridProp.rowGap)) + 1;\n            const columnStart = Math.round(left / (gridProp.columnSize + gridProp.columnGap)) + 1;\n            const rowEnd = rowStart + this.dragState.columnRowCount;\n            const columnEnd = columnStart + this.dragState.columnColCount;\n\n            this.$target[0].style.gridArea = `${rowStart} / ${columnStart} / ${rowEnd} / ${columnEnd}`;\n\n            // Cleaning, removing the drag helper and the background grid and\n            // resizing the grid.\n            gridUtils._gridCleanUp(rowEl, this.$target[0]);\n            this.dragState.dragHelperEl.remove();\n            this.dragState.backgroundGridEl.remove();\n            this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n            gridUtils._resizeGrid(rowEl);\n            this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n        } else if (this.$target[0].classList.contains('o_grid_item') && this.dropped) {\n            // Case when dropping a grid item in a non-grid dropzone.\n            this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n            gridUtils._convertToNormalColumn(this.$target[0]);\n            this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n        }\n\n        // TODO lot of this is duplicated code of the d&d feature of snippets\n        if (!this.dropped) {\n            let $el = $(closest(this.$body[0].querySelectorAll('.oe_drop_zone'), {x, y}));\n            // Some drop zones might have been disabled.\n            $el = $el.filter(this.$dropZones);\n            if ($el.length) {\n                $el.after(this.$target);\n                // If the column is not dropped inside a dropzone.\n                if ($el[0].classList.contains('oe_grid_zone')) {\n                    // Case when a column is dropped near a grid.\n                    const rowEl = $el[0].parentNode;\n\n                    // If the column doesn't come from a snippet in grid mode,\n                    // convert it.\n                    if (!this.$target[0].classList.contains('o_grid_item')) {\n                        this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                        const spans = gridUtils._convertColumnToGrid(rowEl, this.$target[0], this.dragState.columnWidth, this.dragState.columnHeight);\n                        this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n                        this.dragState.columnColCount = spans.columnColCount;\n                        this.dragState.columnRowCount = spans.columnRowCount;\n                    }\n\n                    // Placing it in the top left corner.\n                    this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                    this.$target[0].style.gridArea = `1 / 1 / ${1 + this.dragState.columnRowCount} / ${1 + this.dragState.columnColCount}`;\n                    const rowCount = Math.max(rowEl.dataset.rowCount, this.dragState.columnRowCount);\n                    rowEl.dataset.rowCount = rowCount;\n                    this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n\n                    // Setting the grid item z-index.\n                    if (rowEl === this.dragState.startingGrid) {\n                        this.$target[0].style.zIndex = this.dragState.startingZIndex;\n                    } else {\n                        gridUtils._setElementToMaxZindex(this.$target[0], rowEl);\n                    }\n                } else {\n                    if (this.$target[0].classList.contains('o_grid_item')) {\n                        // Case when a grid column is dropped near a non-grid\n                        // dropzone.\n                        this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n                        gridUtils._convertToNormalColumn(this.$target[0]);\n                        this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n                    }\n                }\n\n                this.dropped = true;\n            }\n        }\n\n        // Resize the grid from where the column came from (if any), as it may\n        // have not been resized if the column did not go over it.\n        if (this.dragState.startingGrid) {\n            this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n            gridUtils._resizeGrid(this.dragState.startingGrid);\n            this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet');\n        }\n\n        this.$editable.find('.oe_drop_zone').remove();\n\n        var prev = this.$target.first()[0].previousSibling;\n        var next = this.$target.last()[0].nextSibling;\n        var $parent = this.$target.parent();\n\n        var $clone = this.$editable.find('.oe_drop_clone');\n        if (prev === $clone[0]) {\n            prev = $clone[0].previousSibling;\n        } else if (next === $clone[0]) {\n            next = $clone[0].nextSibling;\n        }\n        $clone.after(this.$target);\n        var $from = $clone.parent();\n\n        this.$el.removeClass('d-none');\n        this.$body.removeClass('move-important');\n        $clone.remove();\n\n        this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet');\n        if (this.dropped) {\n            if (prev) {\n                this.$target.insertAfter(prev);\n            } else if (next) {\n                this.$target.insertBefore(next);\n            } else {\n                $parent.prepend(this.$target);\n            }\n\n            for (var i in this.styles) {\n                this.styles[i].onMove();\n            }\n\n            // If the target has a mobile order class, and if it was dropped in\n            // another snippet, fill the gap left in the starting snippet.\n            if (this.dragState.mobileOrder !== undefined\n                && this.$target[0].parentNode !== this.dragState.startingParent) {\n                ColumnLayoutMixin._fillRemovedItemGap(this.dragState.startingParent, this.dragState.mobileOrder);\n            }\n\n            this.$target.trigger('content_changed');\n            $from.trigger('content_changed');\n        }\n\n        this.trigger_up('drag_and_drop_stop', {\n            $snippet: this.$target,\n        });\n        const samePositionAsStart = this.$target[0].classList.contains('o_grid_item')\n            ? (this.$target[0].parentNode === this.dragState.startingGrid\n                && this.$target[0].style.gridArea === this.dragState.prevGridArea)\n            : this._dropSiblings.prev === this.$target.prev()[0] && this._dropSiblings.next === this.$target.next()[0];\n        if (!samePositionAsStart) {\n            this.options.wysiwyg.odooEditor.historyStep();\n        }\n\n        this.dragState.restore();\n\n        delete this.$dropZones;\n        delete this.dragState;\n    },\n    /**\n     * @private\n     */\n    _onOptionsSectionMouseEnter: function (ev) {\n        if (!this.$target.is(':visible')) {\n            return;\n        }\n        this.trigger_up('activate_snippet', {\n            $snippet: this.$target,\n            previewMode: true,\n        });\n    },\n    /**\n     * @private\n     */\n    _onOptionsSectionMouseLeave: function (ev) {\n        this.trigger_up('activate_snippet', {\n            $snippet: false,\n            previewMode: true,\n        });\n    },\n    /**\n     * @private\n     */\n    _onOptionsSectionClick: function (ev) {\n        this.trigger_up('activate_snippet', {\n            $snippet: this.$target,\n            previewMode: false,\n        });\n    },\n    /**\n     * Called when a child editor/option asks for another option to perform a\n     * specific action/react to a specific event.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onOptionUpdate: function (ev) {\n        var self = this;\n\n        // If multiple option names are given, we suppose it should not be\n        // propagated to parent editor\n        if (ev.data.optionNames) {\n            ev.stopPropagation();\n            ev.data.optionNames.forEach((name) => {\n                notifyForEachMatchedOption(name);\n            });\n        }\n        // If one option name is given, we suppose it should be handle by the\n        // first parent editor which can do it\n        if (ev.data.optionName) {\n            if (notifyForEachMatchedOption(ev.data.optionName)) {\n                ev.stopPropagation();\n            }\n        }\n\n        function notifyForEachMatchedOption(name) {\n            var regex = new RegExp('^' + name + '\\\\d+$');\n            var hasOption = false;\n            for (var key in self.styles) {\n                if (key === name || regex.test(key)) {\n                    self.styles[key].notify(ev.data.name, ev.data.data);\n                    hasOption = true;\n                }\n            }\n            return hasOption;\n        }\n    },\n    /**\n     * Called when the 'remove' button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onRemoveClick: function (ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        this.trigger_up('snippet_edition_request', {exec: this.removeSnippet.bind(this)});\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSnippetOptionVisibilityUpdate: function (ev) {\n        if (this.options.wysiwyg.isSaving()) {\n            // Do not update the option visibilities if we are destroying them.\n            return;\n        }\n        ev.data.show = this._toggleVisibilityStatus(ev.data.show);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onUserValueWidgetRequest: function (ev) {\n        for (const key of Object.keys(this.styles)) {\n            const widget = this.styles[key].findWidget(ev.data.name);\n            if (widget) {\n                ev.stopPropagation();\n                ev.data.onSuccess(widget);\n                return;\n            }\n        }\n        if (!ev.data.allowParentOption) {\n            ev.stopPropagation();\n        }\n    },\n    /**\n     * Called when the 'mouse wheel' is used when hovering over the overlay.\n     * Disable the pointer events to prevent page scrolling from stopping.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onMouseWheel: function (ev) {\n        ev.stopPropagation();\n        this.$el.css('pointer-events', 'none');\n        clearTimeout(this.wheelTimeout);\n        this.wheelTimeout = setTimeout(() => {\n            this.$el.css('pointer-events', '');\n        }, 250);\n    },\n    /**\n     * Called when the \"send to back\" overlay button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onSendBackClick(ev) {\n        ev.stopPropagation();\n        const rowEl = this.$target[0].parentNode;\n        const columnEls = [...rowEl.children].filter(el => el !== this.$target[0]);\n        const minZindex = Math.min(...columnEls.map(el => el.style.zIndex));\n\n        // While the minimum z-index is not 0, it is OK to decrease it and to\n        // set the column to it. Otherwise, the column is set to 0 and the\n        // other columns z-index are increased by one.\n        if (minZindex > 0) {\n            this.$target[0].style.zIndex = minZindex - 1;\n        } else {\n            for (const columnEl of columnEls) {\n                columnEl.style.zIndex++;\n            }\n            this.$target[0].style.zIndex = 0;\n        }\n    },\n    /**\n     * Called when the \"bring to front\" overlay button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onBringFrontClick(ev) {\n        ev.stopPropagation();\n        const rowEl = this.$target[0].parentNode;\n        gridUtils._setElementToMaxZindex(this.$target[0], rowEl);\n    },\n    /**\n     * Called when the mouse is moved to place a column in a grid.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onDragMove({ x, y }) {\n        if (!this.dragState.gridMode || !this.dragState.currentDropzoneEl) {\n            return;\n        }\n        const columnEl = this.$target[0];\n        const rowEl = columnEl.parentNode;\n\n        // Computing the rowEl position.\n        const rowElTop = rowEl.getBoundingClientRect().top;\n        const rowElLeft = rowEl.getBoundingClientRect().left;\n\n        // Getting the column dimensions.\n        const borderWidth = parseFloat(window.getComputedStyle(columnEl).borderWidth);\n        const columnHeight = columnEl.clientHeight + 2 * borderWidth;\n        const columnWidth = columnEl.clientWidth + 2 * borderWidth;\n\n        // Placing the column where the mouse is.\n        let top = y - rowElTop - this.mousePositionYOnElement;\n        const bottom = top + columnHeight;\n        let left = x - rowElLeft - this.mousePositionXOnElement;\n\n        // Horizontal and top overflow.\n        left = clamp(left, 0, rowEl.clientWidth - columnWidth);\n        top = top < 0 ? 0 : top;\n\n        columnEl.style.top = top + 'px';\n        columnEl.style.left = left + 'px';\n\n        // Computing the drag helper corresponding grid area.\n        const gridProp = gridUtils._getGridProperties(rowEl);\n\n        const rowStart = Math.round(top / (gridProp.rowSize + gridProp.rowGap)) + 1;\n        const columnStart = Math.round(left / (gridProp.columnSize + gridProp.columnGap)) + 1;\n        const rowEnd = rowStart + this.dragState.columnRowCount;\n        const columnEnd = columnStart + this.dragState.columnColCount;\n\n        const dragHelperEl = this.dragState.dragHelperEl;\n        if (parseInt(dragHelperEl.style.gridRowStart) !== rowStart) {\n            dragHelperEl.style.gridRowStart = rowStart;\n            dragHelperEl.style.gridRowEnd = rowEnd;\n        }\n\n        if (parseInt(dragHelperEl.style.gridColumnStart) !== columnStart) {\n            dragHelperEl.style.gridColumnStart = columnStart;\n            dragHelperEl.style.gridColumnEnd = columnEnd;\n        }\n\n        // Vertical overflow/underflow.\n        // Updating the reference heights, the dropzone and the background grid.\n        const startingHeight = this.dragState.startingHeight;\n        const currentHeight = this.dragState.currentHeight;\n        const backgroundGridEl = this.dragState.backgroundGridEl;\n        const dropzoneEl = this.dragState.currentDropzoneEl;\n        const rowOverflow = Math.round((bottom - currentHeight) / (gridProp.rowSize + gridProp.rowGap));\n        const updateRows = bottom > currentHeight || bottom <= currentHeight && bottom > startingHeight;\n        const rowCount = Math.max(rowEl.dataset.rowCount, this.dragState.columnRowCount);\n        const maxRowEnd = rowCount + gridUtils.additionalRowLimit + 1;\n        if (Math.abs(rowOverflow) >= 1 && updateRows) {\n            if (rowEnd <= maxRowEnd) {\n                const dropzoneEnd = parseInt(dropzoneEl.style.gridRowEnd);\n                dropzoneEl.style.gridRowEnd = dropzoneEnd + rowOverflow;\n                backgroundGridEl.style.gridRowEnd = dropzoneEnd + rowOverflow;\n                this.dragState.currentHeight += rowOverflow * (gridProp.rowSize + gridProp.rowGap);\n            } else {\n                // Don't add new rows if we have reached the limit.\n                dropzoneEl.style.gridRowEnd = maxRowEnd;\n                backgroundGridEl.style.gridRowEnd = maxRowEnd;\n                this.dragState.currentHeight = (maxRowEnd - 1) * (gridProp.rowSize + gridProp.rowGap) - gridProp.rowGap;\n            }\n        }\n    },\n    /**\n     * Called when the \"replace\" overlay button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onReplaceClick(ev) {\n        ev.stopPropagation();\n        this.trigger_up('open_add_snippet_dialog', {\n            initialSnippetEl: this.$target[0],\n        });\n    },\n});\n\n/**\n * Management of drag&drop menu and snippet related behaviors in the page.\n */\nclass SnippetsMenu extends Component {\n    static cacheSnippetTemplate = {};\n    static custom_events = {\n        'activate_insertion_zones': '_onActivateInsertionZones',\n        'activate_snippet': '_onActivateSnippet',\n        'call_for_each_child_snippet': '_onCallForEachChildSnippet',\n        'clone_snippet': '_onCloneSnippet',\n        \"clean_ui_request\": \"_onCleanUIRequest\",\n        'cover_update': '_onOverlaysCoverUpdate',\n        'deactivate_snippet': '_onDeactivateSnippet',\n        'drag_and_drop_stop': '_onSnippetDragAndDropStop',\n        'drag_and_drop_start': '_onSnippetDragAndDropStart',\n        'get_snippet_versions': '_onGetSnippetVersions',\n        'find_snippet_template': '_onFindSnippetTemplate',\n        'is_element_selected': '_onIsElementSelected',\n        'remove_snippet': '_onRemoveSnippet',\n        'snippet_edition_request': '_onSnippetEditionRequest',\n        'snippet_editor_destroyed': '_onSnippetEditorDestroyed',\n        'snippet_removed': '_onSnippetRemoved',\n        'snippet_cloned': '_onSnippetCloned',\n        'snippet_option_update': '_onSnippetOptionUpdate',\n        'snippet_option_visibility_update': '_onSnippetOptionVisibilityUpdate',\n        'snippet_thumbnail_url_request': '_onSnippetThumbnailURLRequest',\n        'request_save': '_onSaveRequest',\n        'hide_overlay': '_onHideOverlay',\n        'block_preview_overlays': '_onBlockPreviewOverlays',\n        'unblock_preview_overlays': '_onUnblockPreviewOverlays',\n        'user_value_widget_opening': '_onUserValueWidgetOpening',\n        'user_value_widget_closing': '_onUserValueWidgetClosing',\n        'reload_snippet_template': '_onReloadSnippetTemplate',\n        'request_editable': '_onRequestEditable',\n        'disable_loading_effect': '_onDisableLoadingEffect',\n        'enable_loading_effect': '_onEnableLoadingEffect',\n        \"update_invisible_dom\": \"_onUpdateInvisibleDom\",\n        \"open_add_snippet_dialog\": \"_onOpenAddSnippetDialog\",\n    };\n    // enum of the SnippetsMenu's tabs.\n    static tabs = {\n        BLOCKS: 'blocks',\n        OPTIONS: 'options',\n        CUSTOM: 'custom',\n    };\n\n    static props = {\n        bus: { type: EventBus },\n        mountedProm: { type: Promise },\n        options: { type: Object },\n        trigger_up: { type: Function },\n        folded: { type: Boolean, optional: true },\n        onSnippetDropped: { type: Function, optional: true },\n        readyToCleanForSave: { type: Function, optional: true },\n        setCSSVariables: { type: Function, optional: true },\n    };\n\n    static defaultProps = {\n        folded: false,\n        onSnippetDropped: () => {},\n        readyToCleanForSave: () => {},\n        setCSSVariables: () => {},\n    };\n\n    static template = \"web_editor.SnippetsMenu\";\n\n    static components = { Toolbar, LinkTools };\n\n    setup() {\n        super.setup(...arguments);\n        this.options = Object.assign({}, this.props.options);\n        this.$body = $((this.options.document || document).body);\n        this.customEvents = SnippetsMenu.custom_events;\n        this.tabs = SnippetsMenu.tabs;\n\n        this.state = useState({\n            showCustomizePanel: false,\n            invisibleElements: [],\n            currentTab: SnippetsMenu.tabs.BLOCKS,\n            toolbarTitle: \"\",\n            showToolbar: false,\n            search: \"\",\n            canUndo: false,\n            canRedo: false,\n        });\n\n        this.snippets = useState(new Map());\n\n        this.snippetsMenuRef = useRef(\"snippets-menu\");\n\n        // Odoo Editor uses the HTML Element to bind commands.\n        this.toolbarWrapperRef = useRef(\"toolbar-wrapper\");\n        // SnippetOptions are still rendered using legacy widgets.\n        // TODO: remove this ref when Options are rendered using OWL.\n        this.customizePanelRef = useRef(\"customize-panel\");\n        // Used for drag and drop of Snippets Thumbnails.\n        this.snippetsAreaRef = useRef(\"snippets-area\");\n\n        this.snippetEditors = [];\n        this._enabledEditorHierarchy = [];\n\n        this._mutex = this.options.mutex;\n\n        this._notActivableElementsSelector = [\n            '#web_editor-top-edit',\n            '.o_we_website_top_actions',\n            '#oe_snippets',\n            '#oe_manipulators',\n            '.o_technical_modal',\n            '.oe_drop_zone',\n            '.o_notification_manager',\n            '.o_we_no_overlay',\n            '.ui-autocomplete',\n            '.modal .btn-close',\n            '.o_we_crop_widget',\n            '.transfo-container',\n            '.o_datetime_picker',\n        ].join(', ');\n\n        this.loadingTimers = {};\n        this.loadingElements = {};\n        this._loadingEffectDisabled = false;\n        this._onClick = this._onClick.bind(this);\n\n        this.options.reloadSnippetDropzones = this.reloadSnippetDropzones.bind(this);\n\n        this.orm = useService(\"orm\");\n        this.notification = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.popover = useService(\"popover\");\n\n        onWillStart(() => {\n            // Preload colorpalette dependencies without waiting for them. The\n            // widget have huge chances of being used by the user (clicking on any\n            // text will load it). The colorpalette itself will do the actual\n            // waiting of the loading completion.\n            this.options.wysiwyg.getColorpickerTemplate();\n            this.options.wysiwyg.toolbarEl.classList.add(\"d-none\");\n        });\n\n        onMounted(async () => {\n            await this.start();\n            this.props.setCSSVariables(this.snippetsMenuRef.el);\n\n            // Bind removeFormat button\n            const titleButtons = this.customizePanel.querySelector(\"#o_we_editor_toolbar_container > we-title\");\n            this.options.wysiwyg.odooEditor.bindExecCommand(titleButtons);\n\n            // Get table container and bind commands to Odoo Editor.\n            const customizeTableBlock = this.customizePanel.querySelector('#o-we-editor-table-container');\n            this.options.wysiwyg.odooEditor.bindExecCommand(customizeTableBlock);\n            // TODO: Remove this and instead, use a callback once the editor is\n            // ready, or make the parent component independent of SnippetsMenu\n            // being mounted.\n            this.props.mountedProm.resolve();\n            this.el.classList.add(\"o_loaded\");\n            this.el.ownerDocument.body.classList.toggle('editor_has_snippets', !this.folded);\n        });\n\n        onWillUnmount(() => {\n            this.onWillUnmount();\n        });\n\n        useEffect(\n            (folded) => {\n                this.setFolded(folded);\n            },\n            () => [this.props.folded]\n        );\n\n        useBus(this.props.bus, \"ACTIVATE_SNIPPET\", ({ detail }) => {\n            const { $snippet, previewMode, onSuccess } = detail;\n            this._activateSnippet($snippet, previewMode).then(onSuccess);\n        });\n\n        useBus(this.props.bus, \"CALL_POST_SNIPPET_DROP\", ({ detail }) => {\n            this.callPostSnippetDrop(detail.$snippet).then(detail.onSuccess);\n        });\n\n        useBus(this.props.bus, \"INSERT_SNIPPET\", ({ detail }) => {\n            const { snippetSelector, block } = detail;\n            this._execWithLoadingEffect(() => {\n                const snippet = [...this.snippets.values()].find((snippet) => {\n                    return snippet.baseBody.matches(snippetSelector);\n                });\n                if (snippet && block) {\n                    const clonedBody = snippet.baseBody.cloneNode(true);\n                    clonedBody.classList.remove(\".oe_snippet_body\");\n                    block.after(clonedBody);\n                    // This call will block the mutex so it is not awaited.\n                    this.callPostSnippetDrop($(clonedBody));\n                }\n            });\n        });\n\n        useBus(this.props.bus, \"CLEAN_FOR_SAVE\", ({ detail }) => {\n            detail.proms.push(this.cleanForSave());\n        });\n\n        useBus(this.props.bus, \"UPDATE_SCROLLING_ELEMENT\", ({ detail }) => {\n            this.draggableComponent?.update({ scrollingElement: detail.scrollingElement });\n        });\n    }\n    /**\n     * By default, the SnippetCache is only invalidated when the browser is\n     * refreshed. This should be overridden by apps that want to reset the cache\n     * on some events (e.g. in website, the prop is overridden and instead the\n     * website service is used.)\n     *\n     * @property {Boolean} - true if the cache is valid, false otherwise\n     */\n    get invalidateSnippetCache() {\n        return !!this._invalidateSnippetCache;\n    }\n    set invalidateSnippetCache(invalidate) {\n        this._invalidateSnippetCache = invalidate;\n    }\n    get hasSnippetGroups() {\n        return Array.from(this.snippets.values()).some(snippet => snippet.snippetGroup);\n    }\n    _createTooltip($el, title, selector = false) {\n        return new Tooltip($el, {\n            title: title,\n            selector: selector,\n            placement: \"bottom\",\n            delay: 100,\n            // Ensure the tooltips have a good position when in iframe.\n            container: this.el,\n            // Prevent horizontal scroll when tooltip is displayed.\n            boundary: this.el.ownerDocument.body,\n        });\n    }\n    /**\n     * Method called when the SnippetsMenu is mounted.\n     * At this stage, references at accessible.\n     * It fetches and parses the snippets templates and options, as well as\n     * going through a first pass of the invisible elements.\n     * It also initializes click events on the documents, and sets up tooltips.\n     */\n    async start() {\n        // TODO: at a later date, we should remove this.$el (maybe when jQuery\n        // is removed) and instead use individual refs or use OWL.\n        this.el = this.snippetsMenuRef.el;\n        this.$el = $(this.el);\n        this.$ = this.$el.find.bind(this.$el);\n        this.customizePanel = this.customizePanelRef.el;\n\n        const defs = [];\n        this.ownerDocument = this.$el[0].ownerDocument;\n        this.$document = $(this.ownerDocument);\n        this.window = this.ownerDocument.defaultView;\n        this.$window = $(this.window);\n        // In an iframe, we need to make sure the element is using jquery on its\n        // own window and not on the top window lest jquery behave unexpectedly.\n        this.$el = this.window.$(this.$el);\n        this.$el.data('snippetMenu', this);\n        // We need to activate the touch events to be able to drag and drop\n        // snippets on devices with a touch screen.\n        this.__onTouchEvent = this._onTouchEvent.bind(this);\n        document.addEventListener(\"touchstart\", this.__onTouchEvent, true);\n        document.addEventListener(\"touchmove\", this.__onTouchEvent, true);\n        document.addEventListener(\"touchend\", this.__onTouchEvent, true);\n\n        this._toolbarWrapperEl = this.toolbarWrapperRef.el;\n        this._toolbarWrapperEl.style.display = 'contents';\n\n        const toolbarEl = this._toolbarWrapperEl.firstChild;\n        toolbarEl.classList.remove('oe-floating');\n        this.options.wysiwyg.setupToolbar(toolbarEl);\n        this._addToolbar();\n        this._checkEditorToolbarVisibilityCallback = this._checkEditorToolbarVisibility.bind(this);\n        $(this.options.wysiwyg.odooEditor.document.body).on('click', this._checkEditorToolbarVisibilityCallback);\n\n        // Prepare snippets editor environment\n        this.$snippetEditorArea = $('<div/>', {\n            id: 'oe_manipulators',\n        });\n        this.$body.prepend(this.$snippetEditorArea);\n        this.options.getDragAndDropOptions = this._getDragAndDropOptions.bind(this);\n\n        // Add tooltips on we-title elements whose text overflows and on all\n        // elements with available tooltip text. Note that the tooltips of the\n        // blocks should not be taken into account here because they have\n        // tooltips with a particular behavior (see _showSnippetTooltip).\n        this.tooltips = this._createTooltip(\n            this.el,\n            function () {\n                // Workaround BS regression: https://github.com/twbs/bootstrap/issues/38720\n                const el = this === undefined ? arguments[0] : this.el;\n                // On Firefox, el.scrollWidth is equal to el.clientWidth when\n                // overflow: hidden, so we need to update the style before to\n                // get the right values.\n                el.style.setProperty('overflow', 'scroll', 'important');\n                const tipContent = el.scrollWidth > el.clientWidth ? el.innerHTML : '';\n                el.style.removeProperty('overflow');\n                return tipContent;\n            },\n            \"we-title\"\n        );\n\n        this.buttonTooltips = [];\n        // Before boostrap 5.3, sub tooltips were working by default with hover\n        // behavior, now it is not the case so we instantiate them 1 by 1.\n        document.querySelectorAll(\"[title]:not(.oe_snippet)\").forEach(el => {\n            this.buttonTooltips.push(this._createTooltip(\n                el,\n                function () {\n                    // Workaround BS regression: https://github.com/twbs/bootstrap/issues/38720\n                    const el = this === undefined ? arguments[0] : this.el;\n                    return el.title;\n                }\n            ));\n        });\n\n        // Active snippet editor on click in the page\n        this.$document.on('click.snippets_menu', '*', this._onClick);\n        // Needed as bootstrap stop the propagation of click events for dropdowns\n        this.$document.on('mouseup.snippets_menu', '.dropdown-toggle', this._onClick);\n        // Also listen for clicks inside iframes.\n        if (this.$body[0].ownerDocument !== this.ownerDocument) {\n            this.$body.on('click.snippets_menu', '*', this._onClick);\n        }\n        // Adapt overlay covering when the window is resized / content changes\n        this.debouncedCoverUpdate = throttleForAnimation(() => {\n            this.updateCurrentSnippetEditorOverlay();\n        });\n        this.$window.on(\"resize.snippets_menu\", this.debouncedCoverUpdate);\n        this.$body.on(\"content_changed.snippets_menu\", this.debouncedCoverUpdate);\n        $(this.$body[0].ownerDocument.defaultView).on(\n            \"resize.snippets_menu\",\n            this.debouncedCoverUpdate\n        );\n\n        // On keydown add a class on the active overlay to hide it and show it\n        // again when the mouse moves\n        this.$body.on('keydown.snippets_menu', () => {\n            this.__overlayKeyWasDown = true;\n            this.snippetEditors.forEach(editor => {\n                editor.toggleOverlayVisibility(false);\n            });\n        });\n        this.$body.on('mousemove.snippets_menu, mousedown.snippets_menu', throttleForAnimation(() => {\n            if (!this.__overlayKeyWasDown) {\n                return;\n            }\n            this.__overlayKeyWasDown = false;\n            this.snippetEditors.forEach(editor => {\n                editor.toggleOverlayVisibility(true);\n                editor.cover();\n            });\n        }));\n\n        // Hide the active overlay when scrolling.\n        // Show it again and recompute all the overlays after the scroll.\n        this.$scrollingElement = $().getScrollingElement(this.$body[0].ownerDocument);\n        if (!this.$scrollingElement[0]) {\n            this.$scrollingElement = $(this.ownerDocument).find('.o_editable');\n        }\n        this.$scrollingTarget = $().getScrollingTarget(this.$scrollingElement);\n        this._onScrollingElementScroll = throttleForAnimation(() => {\n            for (const editor of this.snippetEditors) {\n                editor.toggleOverlayVisibility(false);\n            }\n            clearTimeout(this.scrollingTimeout);\n            this.scrollingTimeout = setTimeout(() => {\n                this._scrollingTimeout = null;\n                for (const editor of this.snippetEditors) {\n                    editor.toggleOverlayVisibility(true);\n                    editor.cover();\n                }\n            }, 250);\n        });\n        // We use addEventListener instead of jQuery because we need 'capture'.\n        // Setting capture to true allows to take advantage of event bubbling\n        // for events that otherwise don\u2019t support it. (e.g. useful when\n        // scrolling a modal)\n        this.$scrollingTarget[0].addEventListener('scroll', this._onScrollingElementScroll, {capture: true});\n\n        if (this.options.enableTranslation) {\n            // Load the sidebar with the style tab only.\n            await this._loadSnippetsTemplates();\n            defs.push(this._updateInvisibleDOM());\n            this.state.currentTab = SnippetsMenu.tabs.OPTIONS;\n            this.$el.find('.o_we_website_top_actions').removeClass('d-none');\n            this.$('#snippets_menu button').removeClass('active').prop('disabled', true);\n            this.$('.o_we_customize_snippet_btn').addClass('active').prop('disabled', false);\n            this.$('o_we_ui_loading').addClass('d-none');\n            this.state.showToolbar = false;\n            this.$('#o-we-editor-table-container').addClass('d-none');\n            return Promise.all(defs);\n        }\n\n        this.emptyOptionsTabContent = document.createElement('div');\n        this.emptyOptionsTabContent.classList.add('text-center', 'pt-5');\n        this.emptyOptionsTabContent.append(_t(\"Select a block on your page to style it.\"));\n\n        // Fetch snippet templates and compute it\n        defs.push((async () => {\n            await this._loadSnippetsTemplates();\n            await this._updateInvisibleDOM();\n        })());\n\n        // Auto-selects text elements with a specific class and remove this\n        // on text changes\n        const alreadySelectedElements = new Set();\n        this.$body.on('click.snippets_menu', '.o_default_snippet_text', ev => {\n            const el = ev.currentTarget;\n            if (alreadySelectedElements.has(el)) {\n                // If the element was already selected in such a way before, we\n                // don't reselect it. This actually allows to have the first\n                // click on an element to select its text, but the second click\n                // to place the cursor inside of that text.\n                return;\n            }\n            alreadySelectedElements.add(el);\n            $(el).selectContent();\n        });\n        this.$body.on('keyup.snippets_menu', () => {\n            // Note: we cannot listen to keyup in .o_default_snippet_text\n            // elements via delegation because keyup only bubbles from focusable\n            // elements which contenteditable are not.\n            const selection = this.$body[0].ownerDocument.getSelection();\n            if (!selection.rangeCount) {\n                return;\n            }\n            const range = selection.getRangeAt(0);\n            const $defaultTextEl = $(range.startContainer).closest('.o_default_snippet_text');\n            $defaultTextEl.removeClass('o_default_snippet_text');\n            alreadySelectedElements.delete($defaultTextEl[0]);\n        });\n        const refreshSnippetEditors = debounce(() => {\n            for (const snippetEditor of this.snippetEditors) {\n                this._mutex.exec(() => snippetEditor.destroy());\n            }\n            // FIXME should not the snippetEditors list be emptied here ?\n            const selection = this.$body[0].ownerDocument.getSelection();\n            if (selection.rangeCount) {\n                const target = selection.getRangeAt(0).startContainer.parentElement;\n                this._activateSnippet($(target));\n            }\n\n            this._updateInvisibleDOM();\n        }, 500);\n        this.options.wysiwyg.odooEditor.addEventListener('historyUndo', refreshSnippetEditors);\n        this.options.wysiwyg.odooEditor.addEventListener('historyRedo', refreshSnippetEditors);\n\n        const $autoFocusEls = $('.o_we_snippet_autofocus');\n        this._activateSnippet($autoFocusEls.length ? $autoFocusEls.first() : false);\n\n        return Promise.all(defs).then(() => {\n            const updateHistoryButtons = () => {\n                this.state.canRedo = this.options.wysiwyg.odooEditor.historyCanRedo();\n                this.state.canUndo = this.options.wysiwyg.odooEditor.historyCanUndo();\n            };\n            this.options.wysiwyg.odooEditor.addEventListener('historyStep', updateHistoryButtons);\n            this.options.wysiwyg.odooEditor.addEventListener('observerApply', () => {\n                $(this.options.wysiwyg.odooEditor.editable).trigger('content_changed');\n            });\n            // Trigger a resize event once entering edit mode as the snippets\n            // menu will take part of the screen width (delayed because of\n            // animation). (TODO wait for real animation end)\n            setTimeout(() => {\n                this.$window[0].dispatchEvent(new Event(\"resize\"));\n            }, 1000);\n        });\n    }\n    /**\n     * Called prior to unmounting to remove any event handlers and clean the\n     * DOM.\n     */\n    onWillUnmount() {\n        // Remove listeners for touch events.\n        document.removeEventListener(\"touchstart\", this.__onTouchEvent, true);\n        document.removeEventListener(\"touchmove\", this.__onTouchEvent, true);\n        document.removeEventListener(\"touchend\", this.__onTouchEvent, true);\n        this.draggableComponent && this.draggableComponent.destroy();\n        if (this.$window) {\n            if (this.$snippetEditorArea) {\n                this.$snippetEditorArea.remove();\n            }\n            this.$window.off('.snippets_menu');\n            this.$document.off('.snippets_menu');\n\n            if (this.$scrollingTarget) {\n                this.$scrollingTarget[0].removeEventListener('scroll', this._onScrollingElementScroll, {capture: true});\n            }\n        }\n        if (this.debouncedCoverUpdate) {\n            this.debouncedCoverUpdate.cancel();\n        }\n        $(document.body).off('click', this._checkEditorToolbarVisibilityCallback);\n        this.el.ownerDocument.body.classList.remove('editor_has_snippets');\n        // Dispose BS tooltips.\n        this.tooltips.dispose();\n        this.buttonTooltips.forEach(tooltip => tooltip.dispose());\n        options.clearServiceCache();\n        options.clearControlledSnippets();\n        if (this.$body[0].ownerDocument !== this.ownerDocument) {\n            this.$body.off('.snippets_menu');\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Prepares the page so that it may be saved:\n     * - Asks the snippet editors to clean their associated snippet\n     * - Remove the 'contentEditable' attributes\n     */\n    async cleanForSave() {\n        // Wait for snippet post-drop code here, since sometimes we save very\n        // quickly after a snippet drop during automated testing, which breaks\n        // some options code (executed while destroying the editor).\n        // TODO we should find a better way, by better locking the drag and drop\n        // code inside the edition mutex... which unfortunately cannot be done\n        // given the state of the code, as internal operations of that drag and\n        // drop code need to use the mutex themselves.\n        await this.postSnippetDropPromise;\n\n        // First disable the snippet selection, calling options onBlur, closing\n        // widgets, etc. Then wait for full resolution of the mutex as widgets\n        // may have triggered some final edition requests that need to be\n        // processed before actual \"clean for save\" and saving.\n        await this._activateSnippet(false);\n        await this._mutex.getUnlockedDef();\n\n        // Next, notify that we want the DOM to be cleaned (e.g. in website this\n        // may be the moment where the public widgets need to be destroyed).\n        this.props.readyToCleanForSave();\n        // Wait for the mutex a second time as some options do editor actions when\n        // their snippets are destroyed. (E.g. s_popup triggers visibility updates\n        // when hidden, destroying the widget hides it.)\n        await this._mutex.getUnlockedDef();\n\n        // Then destroy all snippet editors, making them call their own\n        // \"clean for save\" methods (and options ones).\n        await this._destroyEditors();\n\n        // Final editor cleanup\n        this.getEditableArea().find('[contentEditable]')\n            .removeAttr('contentEditable')\n            .removeProp('contentEditable');\n        this.getEditableArea().find('.o_we_selected_image')\n            .removeClass('o_we_selected_image');\n        [...this.getEditableArea()].forEach(editableAreaEl => {\n            editableAreaEl.querySelectorAll(\"[data-visibility='conditional']\")\n                            .forEach(invisibleEl => delete invisibleEl.dataset.invisible);\n        });\n    }\n    /**\n     * Load snippets.\n     */\n    loadSnippets() {\n        if (!this.invalidateSnippetCache && cacheSnippetTemplate[this.options.snippets]) {\n            this._defLoadSnippets = cacheSnippetTemplate[this.options.snippets];\n            return this._defLoadSnippets;\n        }\n        const context = Object.assign({}, this.options.context, {rendering_bundle: true});\n        if (context.user_lang) {\n            context.lang = this.options.context.user_lang;\n            context.snippet_lang = this.options.context.lang;\n        }\n        this._defLoadSnippets = this.orm.silent.call(\n            \"ir.ui.view\",\n            \"render_public_asset\",\n            [this.options.snippets, {}],\n            { context }\n        );\n        cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;\n        if (this.invalidateSnippetCache) {\n            this.invalidateSnippetCache = false;\n        }\n        return this._defLoadSnippets;\n    }\n    /**\n     * Visually hide or display this snippet menu\n     * @param {boolean} foldState\n     */\n    setFolded(foldState = true) {\n        this.el.classList.toggle('d-none', foldState);\n        this.el.ownerDocument.body.classList.toggle('editor_has_snippets', !foldState);\n        this.folded = !!foldState;\n    }\n    /**\n     * Get the editable area.\n     *\n     * @returns {JQuery}\n     */\n    getEditableArea() {\n        return this.options.wysiwyg.$editable.find(this.options.selectorEditableArea)\n            .add(this.options.wysiwyg.$editable.filter(this.options.selectorEditableArea));\n    }\n    /**\n     * Returns a list of categories, each containing snippets, filtered by\n     * the search string currently in state.\n     *\n     * @returns {Map}\n     */\n    getSnippetsByCategories() {\n        const categories = new Map();\n        let snippets = Array.from(this.snippets.values());\n        let strMatches = null;\n        let hasCustomStructureSnippet = false;\n\n        if (this.hasSnippetGroups) {\n            hasCustomStructureSnippet = snippets\n                .some(snippet => snippet.isCustom\n                    && [\"structure\", \"hybrid\"].includes(snippet.fromCategory));\n            // We only show \"categories\" and \"inner content\" snippets in the side\n            // panel. Other snippets are displayed in the modal.\n            snippets = snippets.filter(snippet => {\n                return snippet.category.id !== \"snippet_structure\"\n                    && snippet.category.id !== \"snippet_custom\"\n                    || snippet.category.id === \"snippet_custom\"\n                    && [\"content\", \"hybrid\"].includes(snippet.fromCategory);\n            });\n        } else {\n            const search = this.state.search.toLowerCase();\n            strMatches = str => !search || str.toLowerCase().includes(search);\n        }\n\n        for (const snippet of snippets) {\n            let categorySnippets = categories.get(snippet.category);\n            if (!categorySnippets) {\n                categorySnippets = [];\n                categories.set(snippet.category, categorySnippets);\n            }\n            let matches = false;\n            if (!this.hasSnippetGroups) {\n                matches = strMatches(snippet.category.text)\n                    || strMatches(snippet.displayName)\n                    || strMatches(snippet.data.oeKeywords || '');\n            } else if (snippet.snippetGroup === \"custom\") {\n                // Hide \"custom\" category if there is no \"custom\" snippets.\n                if (!hasCustomStructureSnippet) {\n                    continue;\n                }\n            }\n            if (this.hasSnippetGroups || matches) {\n                categorySnippets.push(snippet);\n            }\n        }\n\n        return categories;\n    }\n    /**\n     * Updates the cover dimensions of the current snippet editor.\n     */\n    updateCurrentSnippetEditorOverlay() {\n        if (this.snippetEditorDragging) {\n            return;\n        }\n        for (const snippetEditor of this.snippetEditors) {\n            if (snippetEditor.$target.closest('body').length) {\n                snippetEditor.cover();\n                continue;\n            }\n            // Destroy options whose $target are not in the DOM anymore but\n            // only do it once all options executions are done.\n            this._mutex.exec(() => this._destroyEditor(snippetEditor));\n        }\n        this._mutex.exec(() => {\n            if (this.state.currentTab === this.tabs.OPTIONS && !this.snippetEditors.length) {\n                const selection = this.$body[0].ownerDocument.getSelection();\n                const range = selection?.rangeCount && selection.getRangeAt(0);\n                const currentlySelectedNode = range?.commonAncestorContainer;\n                // In some cases (e.g. in translation mode) it's possible to have\n                // all snippet editors destroyed after disabling text options.\n                // We still want to keep the toolbar available in this case.\n                const isEditableTextElementSelected =\n                    currentlySelectedNode?.nodeType === Node.TEXT_NODE &&\n                    !!currentlySelectedNode?.parentNode?.isContentEditable;\n                if (!isEditableTextElementSelected) {\n                    this._activateEmptyOptionsTab();\n                }\n            }\n        });\n    }\n    /**\n     * Public method to activate a snippet.\n     *\n     * @see this._activateSnippet\n     * @param {jQuery} $snippet\n     * @returns {Promise}\n     */\n    activateSnippet($snippet) {\n        return this._activateSnippet($snippet);\n    }\n\n    /**\n     * Postprocesses a snippet node when it has been inserted in the dom.\n     *\n     * @param {jQuery} $target\n     * @param {function} [postSnippetDropExtraActions]\n     *        Additional actions to perform after the snippet is dropped.\n     * @returns {Promise}\n     */\n    async callPostSnippetDrop($target, postSnippetDropExtraActions) {\n        this.postSnippetDropPromise = new Promise(resolve => {\n            this._postSnippetDropResolver = resolve;\n        });\n\n        // First call the onBuilt of all options of each item in the snippet\n        // (and so build their editor instance first).\n        await this._callForEachChildSnippet($target, function (editor, $snippet) {\n            return editor.buildSnippet($target[0]);\n        });\n        // The snippet is now fully built, notify the editor for changed\n        // content.\n        $target.trigger('content_changed');\n\n        // Now notifies that a snippet was dropped (at the moment, useful to\n        // start public widgets for instance (no saved content)).\n        await this._mutex.exec(() => {\n            const proms = [];\n            this.props.onSnippetDropped({\n                $target: $target,\n                addPostDropAsync: prom => proms.push(prom),\n            });\n            return Promise.all(proms);\n        });\n\n        // Lastly, ensure that the snippets or its related parts are added to\n        // the invisible DOM list if needed.\n        await this._updateInvisibleDOM();\n\n        if (postSnippetDropExtraActions) {\n            postSnippetDropExtraActions();\n        }\n        this._postSnippetDropResolver();\n    }\n    /**\n     * Public implementation of _execWithLoadingEffect.\n     *\n     * @see this._execWithLoadingEffect for parameters\n     */\n    execWithLoadingEffect(action, contentLoading = true, delay = 500) {\n        return this._execWithLoadingEffect(...arguments);\n    }\n    reloadSnippetDropzones() {\n        this._disableUndroppableSnippets();\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Creates drop zones in the DOM (locations where snippets may be dropped).\n     * Those locations are determined thanks to the two types of given DOM.\n     *\n     * @private\n     * @param {jQuery} [$selectorSiblings]\n     *        elements which must have siblings drop zones\n     * @param {jQuery} [$selectorChildren]\n     *        elements which must have child drop zones between each of existing\n     *        child\n     * @param {string or boolean} canBeSanitizedUnless\n     *        true: always allows,\n     *        false: always forbid,\n     *        string: specific type of forbidden sanitization\n     * @param {Boolean} [toInsertInline=false]\n     *        elements which are inline as the \"s_badge\" snippet for example\n     * @param {Object} [selectorGrids = []]\n     *        elements which are in grid mode and for which a grid dropzone\n     *        needs to be inserted\n     */\n    _activateInsertionZones($selectorSiblings, $selectorChildren, canBeSanitizedUnless, toInsertInline, selectorGrids = [], fromIframe = false) {\n        var self = this;\n\n        // If a modal or a dropdown is open, the drop zones must be created\n        // only in this element.\n        const $editableArea = self.getEditableArea();\n        let $open = $editableArea.find('.modal:visible');\n        if (!$open.length) {\n            $open = $editableArea.find('.dropdown-menu.show').addBack('.dropdown-menu.show').parent();\n        }\n        if ($open.length) {\n            $selectorSiblings = $open.find($selectorSiblings);\n            $selectorChildren = $open.find($selectorChildren);\n            selectorGrids = new Set([...selectorGrids].filter(rowEl => $open[0].contains(rowEl)));\n        }\n\n        // Check if the drop zone should be horizontal or vertical\n        function setDropZoneDirection($elem, $parent, toInsertInline, $sibling) {\n            let vertical = false;\n            let style = {};\n            $sibling = $sibling || $elem;\n            const css = window.getComputedStyle($elem[0]);\n            const parentCss = window.getComputedStyle($parent[0]);\n            const float = css.float || css.cssFloat;\n            const display = parentCss.display;\n            const flex = parentCss.flexDirection;\n            if (toInsertInline || float === 'left' || float === 'right' || (display === 'flex' && flex === 'row')) {\n                if (!toInsertInline) {\n                    style['float'] = float;\n                }\n                if ((parseInt($sibling.parent().width()) !== parseInt($sibling.outerWidth(true)))) {\n                    vertical = true;\n                    style['height'] = Math.max($sibling.outerHeight(), 30) + 'px';\n                    if (toInsertInline) {\n                        style[\"display\"] = \"inline-block\";\n                        style[\"verticalAlign\"] = \"middle\";\n                        style[\"float\"] = \"none\";\n                    }\n                }\n            }\n            return {\n                vertical: vertical,\n                style: style,\n            };\n        }\n\n        // If the previous sibling is a BR tag or a non-whitespace text, it\n        // should be a vertical dropzone.\n        function testPreviousSibling(node, $zone) {\n            if (!node || ((node.tagName || !node.textContent.match(/\\S/)) && node.tagName !== 'BR')) {\n                return false;\n            }\n            return {\n                vertical: true,\n                style: {\n                    'float': 'none',\n                    'display': 'inline-block',\n                    'height': parseInt(self.window.getComputedStyle($zone[0]).lineHeight) + 'px',\n                },\n            };\n        }\n\n        // Firstly, add a dropzone after the clone (if we are not in grid mode).\n        var $clone = this.$body.find('.oe_drop_clone');\n        if ($clone.length && !$clone[0].parentElement.classList.contains(\"o_grid_mode\")) {\n            var $neighbor = $clone.prev();\n            if (!$neighbor.length) {\n                $neighbor = $clone.next();\n            }\n            var data;\n            if ($neighbor.length) {\n                data = setDropZoneDirection($neighbor, $neighbor.parent(), toInsertInline);\n            } else {\n                data = {\n                    vertical: false,\n                    style: {},\n                };\n            }\n            self._insertDropzone($('<we-hook/>').insertAfter($clone), data.vertical, data.style, canBeSanitizedUnless);\n        }\n        // If a modal or a dropdown is open, add the grid of the clone in the\n        // grid selectors to still be able to drop where the drag started.\n        if ($clone.length && $open.length && $clone[0].parentElement.classList.contains(\"o_grid_mode\")) {\n            selectorGrids.add($clone[0].parentElement);\n        }\n\n        if ($selectorChildren) {\n            $selectorChildren.each(function () {\n                var data;\n                var $zone = $(this);\n                var $children = $zone.find('> :not(.oe_drop_zone, .oe_drop_clone)');\n\n                if (!$zone.children().last().is('.oe_drop_zone')) {\n                    data = testPreviousSibling($zone[0].lastChild, $zone)\n                        || setDropZoneDirection($zone, $zone, toInsertInline, $children.last());\n                    self._insertDropzone($('<we-hook/>').appendTo($zone), data.vertical, data.style, canBeSanitizedUnless);\n                }\n\n                if (!$zone.children().first().is('.oe_drop_clone')) {\n                    data = testPreviousSibling($zone[0].firstChild, $zone)\n                        || setDropZoneDirection($zone, $zone, toInsertInline, $children.first());\n                    self._insertDropzone($('<we-hook/>').prependTo($zone), data.vertical, data.style, canBeSanitizedUnless);\n                }\n            });\n\n            // add children near drop zone\n            $selectorSiblings = $(unique(($selectorSiblings || $()).add($selectorChildren.children()).get()));\n        }\n\n        const noDropZonesSelector = '.o_we_no_overlay, :not(:visible)';\n        if ($selectorSiblings) {\n            $selectorSiblings.not(`.oe_drop_zone, .oe_drop_clone, ${noDropZonesSelector}`).each(function () {\n                var data;\n                var $zone = $(this);\n                var $zoneToCheck = $zone;\n\n                while ($zoneToCheck.prev(noDropZonesSelector).length) {\n                    $zoneToCheck = $zoneToCheck.prev();\n                }\n                if (!$zoneToCheck.prev('.oe_drop_zone:visible, .oe_drop_clone').length) {\n                    data = setDropZoneDirection($zone, $zone.parent(), toInsertInline);\n                    self._insertDropzone($('<we-hook/>').insertBefore($zone), data.vertical, data.style, canBeSanitizedUnless);\n                }\n\n                $zoneToCheck = $zone;\n                while ($zoneToCheck.next(noDropZonesSelector).length) {\n                    $zoneToCheck = $zoneToCheck.next();\n                }\n                if (!$zoneToCheck.next('.oe_drop_zone:visible, .oe_drop_clone').length) {\n                    data = setDropZoneDirection($zone, $zone.parent(), toInsertInline);\n                    self._insertDropzone($('<we-hook/>').insertAfter($zone), data.vertical, data.style, canBeSanitizedUnless);\n                }\n            });\n        }\n\n        var count;\n        var $zones;\n        do {\n            count = 0;\n            $zones = this.getEditableArea().find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones\n            count += $zones.length;\n            $zones.remove();\n        } while (count > 0);\n\n        // Cleaning consecutive zone and up zones placed between floating or\n        // inline elements. We do not like these kind of zones.\n        $zones = this.getEditableArea().find('.oe_drop_zone:not(.oe_vertical)');\n\n        let iframeOffset;\n        const bodyWindow = this.$body[0].ownerDocument.defaultView;\n        if (bodyWindow.frameElement && bodyWindow !== this.ownerDocument.defaultView && !fromIframe) {\n            iframeOffset = bodyWindow.frameElement.getBoundingClientRect();\n        }\n\n        $zones.each(function () {\n            var zone = $(this);\n            var prev = zone.prev();\n            var next = zone.next();\n            // remove consecutive zone\n            if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {\n                zone.remove();\n                return;\n            }\n            var floatPrev = prev.css('float') || 'none';\n            var floatNext = next.css('float') || 'none';\n            var dispPrev = prev.css('display') || null;\n            var dispNext = next.css('display') || null;\n            if ((floatPrev === 'left' || floatPrev === 'right')\n             && (floatNext === 'left' || floatNext === 'right')) {\n                zone.remove();\n            } else if (dispPrev !== null && dispNext !== null\n             && dispPrev.indexOf('inline') >= 0 && dispNext.indexOf('inline') >= 0) {\n                zone.remove();\n            }\n\n            // In the case of the SnippetsMenu being instanciated in the global\n            // document, with its editable content in an iframe, we want to\n            // take the iframe's offset into account to compute the dropzones.\n            if (iframeOffset) {\n                this.oldGetBoundingClientRect = this.getBoundingClientRect;\n                this.getBoundingClientRect = () => {\n                    const rect = this.oldGetBoundingClientRect();\n                    const { x, y } = iframeOffset;\n                    rect.x += x;\n                    rect.y += y;\n                    return rect;\n                };\n            }\n        });\n\n        // Inserting a grid dropzone for each row in grid mode.\n        for (const rowEl of selectorGrids) {\n            self._insertGridDropzone(rowEl);\n        }\n    }\n    /**\n     * Adds an entry for every invisible snippet in the left panel box.\n     * The entries will contains an 'Edit' button to activate their snippet.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    _updateInvisibleDOM() {\n        return this._execWithLoadingEffect(async () => {\n            this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n            this.invisibleDOMMap = new Map();\n            const isMobile = this._isMobile();\n            const invisibleSelector = `.o_snippet_invisible, ${isMobile ? '.o_snippet_mobile_invisible' : '.o_snippet_desktop_invisible'}`;\n            const $selector = this.options.enableTranslation ? this.$body : globalSelector.all();\n            let $invisibleSnippets = $selector.find(invisibleSelector).addBack(invisibleSelector);\n\n            if (this.options.enableTranslation) {\n                // In translate mode, we do not want to be able to activate a\n                // hidden header or footer.\n                $invisibleSnippets = $invisibleSnippets.not(\"header, footer\");\n            }\n\n            // descendantPerSnippet: a map with its keys set to invisible\n            // snippets that have invisible descendants. The value corresponding\n            // to an invisible snippet element is a list filled with all its\n            // descendant invisible snippets except those that have a closer\n            // invisible snippet ancestor.\n            const descendantPerSnippet = new Map();\n            // Filter the \"$invisibleSnippets\" to only keep the root snippets\n            // and create the map (\"descendantPerSnippet\") of the snippets and\n            // their descendant snippets.\n            const rootInvisibleSnippetEls = [...$invisibleSnippets].filter(invisibleSnippetEl => {\n                const ancestorInvisibleEl = invisibleSnippetEl\n                                                 .parentElement.closest(invisibleSelector);\n                if (!ancestorInvisibleEl) {\n                    return true;\n                }\n                const descendantSnippets = descendantPerSnippet.get(ancestorInvisibleEl) || [];\n                descendantPerSnippet.set(ancestorInvisibleEl,\n                    [...descendantSnippets, invisibleSnippetEl]);\n                return false;\n            });\n            // Insert an invisible snippet in its \"parentEl\" element.\n            const createInvisibleElement = async (invisibleSnippetEl, isRootParent, isDescendant, parents) => {\n                const editor = await this._createSnippetEditor($(invisibleSnippetEl), true);\n                return {\n                    editor,\n                    snippetEl: invisibleSnippetEl,\n                    name: editor.getName(),\n                    isRootParent,\n                    isDescendant,\n                    invisibleSnippetEl,\n                    isVisible: editor.isTargetVisible(),\n                    children: [],\n                    parents: isDescendant ? parents : null,\n                };\n            };\n            // Insert all the invisible snippets contained in \"snippetEls\" as\n            // well as their descendants in the \"parentEl\" element. If\n            // \"snippetEls\" is set to \"rootInvisibleSnippetEls\" and \"parentEl\"\n            // is set to \"$invisibleDOMPanelEl[0]\", then fills the right\n            // invisible panel like this:\n            // rootInvisibleSnippet\n            //     \u2514 descendantInvisibleSnippet\n            //          \u2514 descendantOfDescendantInvisibleSnippet\n            //               \u2514 etc...\n            const createInvisibleElements = (snippetEls, isDescendant, parents) => {\n                return Promise.all((snippetEls).map(async (snippetEl) => {\n                    const descendantSnippetEls = descendantPerSnippet.get(snippetEl);\n                    // An element is considered as \"RootParent\" if it has one or\n                    // more invisible descendants but is not a descendant.\n                    const invisibleElement = await createInvisibleElement(snippetEl,\n                        !isDescendant && !!descendantSnippetEls, isDescendant, parents);\n                    if (descendantSnippetEls) {\n                        invisibleElement.children = await createInvisibleElements(descendantSnippetEls, true, invisibleElement);\n                    }\n                    return invisibleElement;\n                }));\n            };\n            this.state.invisibleElements = await createInvisibleElements(rootInvisibleSnippetEls, false, this.state.invisibleElements);\n        }, false);\n    }\n    /**\n     * Disable the overlay editor of the active snippet and activate the new one\n     * if given.\n     * Note 1: if the snippet editor associated to the given snippet is not\n     *         created yet, this method will create it.\n     * Note 2: if the given DOM element is not a snippet (no editor option), the\n     *         first parent which is one is used instead.\n     *\n     * @param {jQuery|false} $snippet\n     *        The DOM element whose editor (and its parent ones) need to be\n     *        enabled. Only disable the current one if false is given.\n     * @param {boolean} [previewMode=false]\n     * @param {boolean} [ifInactiveOptions=false]\n     * @returns {Promise<SnippetEditor>}\n     *          (might be async when an editor must be created)\n     */\n    async _activateSnippet($snippet, previewMode, ifInactiveOptions) {\n        if (this._blockPreviewOverlays && previewMode) {\n            return;\n        }\n        if ($snippet && !$snippet.is(':visible')) {\n            return;\n        }\n        // Take the first parent of the provided DOM (or itself) which\n        // should have an associated snippet editor.\n        // It is important to do that before the mutex exec call to compute it\n        // before potential ancestor removal.\n        if ($snippet && $snippet.length) {\n            const $globalSnippet = globalSelector.closest($snippet);\n            if (!$globalSnippet.length) {\n                $snippet = $snippet.closest('[data-oe-model=\"ir.ui.view\"]:not([data-oe-type]):not(.oe_structure), [data-oe-type=\"html\"]:not(.oe_structure)');\n            } else {\n                $snippet = $globalSnippet;\n            }\n        }\n        if (this.options.enableTranslation && $snippet && !this._allowInTranslationMode($snippet)) {\n            // In translate mode, only activate allowed snippets (e.g., even if\n            // we create editors for invisible elements when translating them,\n            // we only want to toggle their visibility when the related sidebar\n            // buttons are clicked).\n            const translationEditors = this.snippetEditors.filter(editor => {\n                return this._allowInTranslationMode(editor.$target);\n            });\n            // Before returning, we need to clean editors if their snippets are\n            // allowed in the translation mode.\n            for (const editor of translationEditors) {\n                await editor.cleanForSave();\n                editor.destroy();\n            }\n            return;\n        }\n        const exec = previewMode\n            ? action => this._mutex.exec(action)\n            : action => this._execWithLoadingEffect(action, false);\n        return exec(() => {\n            return new Promise(resolve => {\n                if ($snippet && $snippet.length) {\n                    return this._createSnippetEditor($snippet).then(resolve);\n                }\n                resolve(null);\n            }).then(async editorToEnable => {\n                if (!previewMode && this._enabledEditorHierarchy[0] === editorToEnable\n                        || ifInactiveOptions && this._enabledEditorHierarchy.includes(editorToEnable)) {\n                    return editorToEnable;\n                }\n\n                if (!previewMode) {\n                    this._enabledEditorHierarchy = [];\n                    let current = editorToEnable;\n                    while (current && current.$target) {\n                        this._enabledEditorHierarchy.push(current);\n                        current = current.getParent();\n                    }\n                }\n\n                // First disable all editors...\n                for (let i = this.snippetEditors.length; i--;) {\n                    const editor = this.snippetEditors[i];\n                    editor.toggleOverlay(false, previewMode);\n                    if (!previewMode) {\n                        const wasShown = !!await editor.toggleOptions(false);\n                        if (wasShown) {\n                            this._updateRightPanelContent({\n                                content: [],\n                                tab: SnippetsMenu.tabs.BLOCKS,\n                            });\n                        }\n                    }\n                }\n                // ... then enable the right editor or look if some have been\n                // enabled previously by a click\n                let customize$Elements;\n                if (editorToEnable) {\n                    editorToEnable.toggleOverlay(true, previewMode);\n                    if (!previewMode && !editorToEnable.displayOverlayOptions) {\n                        const parentEditor = this._enabledEditorHierarchy.find(ed => ed.displayOverlayOptions);\n                        if (parentEditor) {\n                            parentEditor.toggleOverlay(true, previewMode);\n                        }\n                    }\n                    customize$Elements = await editorToEnable.toggleOptions(true);\n                } else {\n                    for (const editor of this.snippetEditors) {\n                        if (editor.isSticky()) {\n                            editor.toggleOverlay(true, false);\n                            customize$Elements = await editor.toggleOptions(true);\n                        }\n                    }\n                }\n\n                if (!previewMode) {\n                    // As some options can only be generated using JavaScript\n                    // (e.g. 'SwitchableViews'), it may happen at this point\n                    // that the overlay is activated even though there are no\n                    // options. That's why we disable the overlay if there are\n                    // no options to enable.\n                    if (editorToEnable && !customize$Elements) {\n                        editorToEnable.toggleOverlay(false);\n                    }\n                    this._updateRightPanelContent({\n                        content: customize$Elements || [],\n                        tab: customize$Elements ? this.tabs.OPTIONS : this.tabs.BLOCKS,\n                    });\n                }\n\n                return editorToEnable;\n            });\n        });\n    }\n    /**\n     * @private\n     */\n    async _loadSnippetsTemplates(withMutex = true) {\n        const loadSnippetsTemplates = async () => {\n            await this._destroyEditors();\n            const html = await this.loadSnippets();\n            const snippetsDocument = new DOMParser().parseFromString(html, \"text/html\");\n            const snippetsBody = snippetsDocument.body;\n            await this._computeSnippetTemplates(snippetsBody);\n        };\n        if (withMutex) {\n            return this._execWithLoadingEffect(async () => {\n                await loadSnippetsTemplates();\n            }, false);\n        } else {\n            return loadSnippetsTemplates();\n        }\n    }\n    /**\n     * TODO everything related to SnippetEditor destroy / cleanForSave should\n     * really be cleaned / unified.\n     *\n     * @private\n     * @param {SnippetEditor} editor\n     */\n    _destroyEditor(editor) {\n        editor.destroy();\n        const index = this.snippetEditors.indexOf(editor);\n        if (index >= 0) {\n            this.snippetEditors.splice(index, 1);\n        }\n    }\n    /**\n     * @private\n     * @param {jQuery|null|undefined} [$el]\n     *        The DOM element whose inside editors need to be destroyed.\n     *        If no element is given, all the editors are destroyed.\n     */\n    async _destroyEditors($el) {\n        const aliveEditors = this.snippetEditors.filter((snippetEditor) => {\n            return !$el || $el.has(snippetEditor.$target).length;\n        });\n        const cleanForSavePromises = aliveEditors.map((snippetEditor) => snippetEditor.cleanForSave());\n        await Promise.all(cleanForSavePromises);\n\n        for (const snippetEditor of aliveEditors) {\n            // No need to clean the `this.snippetEditors` array as each\n            // individual destroy notifies this class instance to remove the\n            // element from the array.\n            snippetEditor.destroy();\n        }\n    }\n    /**\n     * Calls a given callback 'on' the given snippet and all its child ones if\n     * any (DOM element with options).\n     *\n     * Note: the method creates the snippet editors if they do not exist yet.\n     *\n     * @private\n     * @param {jQuery} $snippet\n     * @param {function} callback\n     *        Given two arguments: the snippet editor associated to the snippet\n     *        being managed and the DOM element of this snippet.\n     * @returns {Promise} (might be async if snippet editors need to be created\n     *                     and/or the callback is async)\n     */\n    _callForEachChildSnippet($snippet, callback) {\n        var self = this;\n        var defs = Array.from($snippet.add(globalSelector.all($snippet))).map((el) => {\n            var $snippet = $(el);\n            return self._createSnippetEditor($snippet).then(function (editor) {\n                if (editor) {\n                    return callback.call(self, editor, $snippet);\n                }\n            });\n        });\n        return Promise.all(defs);\n    }\n    /**\n     * @private\n     */\n    _closeWidgets() {\n        this.snippetEditors.forEach(editor => editor.closeWidgets());\n    }\n    /**\n     * Creates and returns a set of helper functions which can help finding\n     * snippets in the DOM which match some parameters (typically parameters\n     * given by a snippet option). The functions are:\n     *\n     * - `is`: to determine if a given DOM is a snippet that matches the\n     *         parameters\n     *\n     * - `closest`: find closest parent (or itself) of a given DOM which is a\n     *              snippet that matches the parameters\n     *\n     * - `all`: find all snippets in the DOM that match the parameters\n     *\n     * See implementation for function details.\n     *\n     * @private\n     * @param {string} selector\n     *        jQuery selector that DOM elements must match to be considered as\n     *        potential snippet.\n     * @param {string} exclude\n     *        jQuery selector that DOM elements must *not* match to be\n     *        considered as potential snippet.\n     * @param {string|false} target\n     *        jQuery selector that at least one child of a DOM element must\n     *        match to that DOM element be considered as a potential snippet.\n     * @param {boolean} noCheck\n     *        true if DOM elements which are technically not in an editable\n     *        environment may be considered.\n     * @param {boolean} isChildren\n     *        when the DOM elements must be in an editable environment to be\n     *        considered (@see noCheck), this is true if the DOM elements'\n     *        parent must also be in an editable environment to be considered.\n     * @param {string} excludeParent\n     *        jQuery selector that the parents of DOM elements must *not* match\n     *        to be considered as potential snippet.\n     * @param {boolean} forDrop\n     *        true if the selector is used to find a drop zone.\n     * @param {string} textSelector\n     *        a selector that DOM elements must match to be considered as\n     *        \"Text Options\"-related snippets.\n     */\n    _computeSelectorFunctions({selector, exclude, target, noCheck, isChildren, excludeParent, forDrop, textSelector}) {\n        var self = this;\n\n        // The `:not(.o_editable_media)` part is handled outside of the selector\n        // (see filterFunc).\n        // Note: the `:not([contenteditable=\"true\"])` part was there for that\n        // same purpose before the implementation of the o_editable_media class.\n        // It still make sense for potential editable areas though. Although it\n        // should be reviewed if we are to handle more hierarchy of nodes being\n        // editable despite their non editable environment.\n        // Without the `:not(.s_social_media)`, it is no longer possible to edit\n        // icons in the social media snippet. This should be fixed in a more\n        // proper way to get rid of this hack.\n        exclude += `${exclude && ', '}.o_snippet_not_selectable`;\n\n        let filterFunc = function () {\n            if (forDrop) {\n                // Prevents blocks from being dropped into an image field.\n                const selfOrParentEl = isChildren ? this.parentNode : this;\n                if (selfOrParentEl.closest(\"[data-oe-type=image]\")) {\n                    return false;\n                }\n            }\n            // Exclude what it is asked to exclude.\n            if ($(this).is(exclude)) {\n                return false;\n            }\n            if (noCheck) {\n                // When noCheck is true, we only check the exclude.\n                return true;\n            }\n            // `o_editable_media` bypasses the `o_not_editable` class except for\n            // drag & drop.\n            if (!forDrop && this.classList.contains('o_editable_media')) {\n                return weUtils.shouldEditableMediaBeEditable(this);\n            }\n            if (forDrop && !isChildren) {\n                // it's a drop-in.\n                return !$(this)\n                    .is('.o_not_editable :not([contenteditable=\"true\"]), .o_not_editable');\n            }\n            if (isChildren) {\n                return !$(this).is('.o_not_editable *');\n            }\n            return !$(this)\n                .is('.o_not_editable:not(.s_social_media) :not([contenteditable=\"true\"])');\n        };\n        if (target) {\n            const oldFilter = filterFunc;\n            filterFunc = function () {\n                return oldFilter.apply(this) && $(this).find(target).length !== 0;\n            };\n        }\n        if (excludeParent) {\n            const oldFilter = filterFunc;\n            filterFunc = function () {\n                return oldFilter.apply(this) && !$(this).parent().is(excludeParent);\n            };\n        }\n\n        // Prepare the functions\n        const functions = {};\n        // In translate mode, it is only possible to modify text content but not\n        // the structure of the snippets. For this reason, the \"Editable area\"\n        // are only the text zones and they should not be used inside functions\n        // such as \"is\", \"closest\" and \"all\".\n        if (noCheck || this.options.enableTranslation) {\n            functions.is = function ($from, options = {}) {\n                return $from.is(options.onlyTextOptions ? textSelector : selector) && $from.filter(filterFunc).length !== 0;\n            };\n            functions.closest = function ($from, parentNode) {\n                return $from.closest(selector, parentNode).filter(filterFunc);\n            };\n            functions.all = function ($from) {\n                return ($from ? self.cssFind($from, selector) : self.$body.find(selector)).filter(filterFunc);\n            };\n        } else {\n            functions.is = function ($from) {\n                return $from.is(selector)\n                    && self.getEditableArea().find($from).addBack($from).length !== 0\n                    && $from.filter(filterFunc).length !== 0;\n            };\n            functions.closest = function ($from, parentNode) {\n                var parents = self.getEditableArea().get();\n                return $from.closest(selector, parentNode).filter(function () {\n                    var node = this;\n                    while (node.parentNode) {\n                        if (parents.indexOf(node) !== -1) {\n                            return true;\n                        }\n                        node = node.parentNode;\n                    }\n                    return false;\n                }).filter(filterFunc);\n            };\n            functions.all = isChildren ? function ($from) {\n                return self.cssFind($from || self.getEditableArea(), selector).filter(filterFunc);\n            } : function ($from) {\n                $from = $from || self.getEditableArea();\n                return $from.filter(selector).add(self.cssFind($from, selector)).filter(filterFunc);\n            };\n        }\n        return functions;\n    }\n    /**\n     * Processes the given snippet template to register snippet options, creates\n     * draggable thumbnail, etc.\n     *\n     * @private\n     * @param {HTMLElement} html\n     */\n    _computeSnippetTemplates(html) {\n        var self = this;\n        var $html = $(html);\n\n        // TODO: Remove in master and add it in template s_website_form\n        const websiteFormEditorOptionsEl = $html.find('[data-js=\"WebsiteFormEditor\"]')[0];\n        if (websiteFormEditorOptionsEl) {\n            websiteFormEditorOptionsEl.dataset.dropExcludeAncestor = \"form\";\n        }\n        this.templateOptions = [];\n        var selectors = [];\n        var $styles = $html.find('[data-selector]');\n        const snippetAdditionDropIn = $styles.filter('#so_snippet_addition').data('drop-in');\n        $styles.each(function () {\n            var $style = $(this);\n            var selector = $style.data('selector');\n            var exclude = $style.data('exclude') || '';\n            const excludeParent = $style.attr('id') === \"so_content_addition\" ? snippetAdditionDropIn : '';\n            var target = $style.data('target');\n            var noCheck = $style.data('no-check');\n            // Note that the optionID will be used to add a class\n            // `snippet-option-XXX` (XXX being the optionID) on the related\n            // option DOM. This is used in JS tours. The data-js attribute can\n            // be used without a corresponding JS class being defined.\n            const optionID = $style.data('js');\n            const textSelector = $style.data(\"text-selector\");\n            var option = {\n                'option': optionID,\n                'base_selector': selector,\n                'base_exclude': exclude,\n                'base_target': target,\n                'selector': self._computeSelectorFunctions({selector, exclude, target, noCheck, textSelector}),\n                '$el': $style,\n                'drop-near': $style.data('drop-near') && self._computeSelectorFunctions({\n                    selector: $style.data('drop-near'),\n                    noCheck,\n                    isChildren: true,\n                    excludeParent,\n                    forDrop: true\n                }),\n                'drop-in': $style.data('drop-in') && self._computeSelectorFunctions({\n                    selector: $style.data('drop-in'),\n                    noCheck,\n                    forDrop: true,\n                }),\n                'drop-exclude-ancestor': this.dataset.dropExcludeAncestor,\n                'drop-lock-within': this.dataset.dropLockWithin,\n                'data': Object.assign({string: $style.attr('string')}, $style.data()),\n            };\n            self.templateOptions.push(option);\n            selectors.push(option.selector);\n        });\n        $styles.addClass('d-none');\n\n        globalSelector.closest = function ($from) {\n            var $temp;\n            var $target;\n            for (var i = 0, len = selectors.length; i < len; i++) {\n                $temp = selectors[i].closest($from, $target && $target[0]);\n                if ($temp.length) {\n                    $target = $temp;\n                }\n            }\n            return $target || $();\n        };\n        globalSelector.all = function ($from) {\n            var $target = $();\n            for (var i = 0, len = selectors.length; i < len; i++) {\n                $target = $target.add(selectors[i].all($from));\n            }\n            return $target;\n        };\n        globalSelector.is = function ($from, options = {}) {\n            for (var i = 0, len = selectors.length; i < len; i++) {\n                if (selectors[i].is($from, options)) {\n                    return true;\n                }\n            }\n            return false;\n        };\n\n        this.snippets.clear();\n        let index = 0;\n        for (const snippetsEl of html.querySelectorAll(\"snippets\")) {\n            const category = {\n                id: snippetsEl.id,\n                text: snippetsEl.getAttribute(\"string\"),\n                classes: [...snippetsEl.classList]\n            };\n            for (const snippetEl of snippetsEl.children) {\n                const isCustom = !!snippetEl.closest('#snippet_custom');\n                const snippet = {\n                    id: parseInt(snippetEl.dataset.oeSnippetId) || uniqueId(snippetEl.dataset.moduleId),\n                    name: snippetEl.children[0].dataset.snippet,\n                    displayName: snippetEl.getAttribute(\"name\"),\n                    category: category,\n                    content: snippetEl.children,\n                    thumbnailSrc: escape(snippetEl.dataset.oeThumbnail),\n                    imagePreview: escape(snippetEl.dataset.oImagePreview),\n                    visible: true,\n                    baseBody: snippetEl.children[0],\n                    data: {...snippetEl.dataset, ...snippetEl.children[0].dataset},\n                    isCustom: isCustom,\n                    snippetGroup: snippetEl.dataset.oSnippetGroup,\n                    group: isCustom ? \"custom\" : snippetEl.dataset.oGroup,\n                    key: index++,\n                };\n\n                if (snippetEl.dataset.oSnippetGroup) {\n                    snippet.content[0].dataset.snippetGroup = snippetEl.dataset.oSnippetGroup;\n                }\n\n                [...snippet.content].forEach(el => {\n                    el.classList.add(\"oe_snippet_body\");\n                    // Associate in-page snippets to their name\n                    // TODO I am not sure this is useful anymore and it should at\n                    // least be made more robust using data-snippet\n                    let snippetClasses = el.getAttribute('class').match(/s_[^ ]+/g);\n                    if (snippetClasses && snippetClasses.length) {\n                        snippetClasses = '.' + snippetClasses.join('.');\n                    }\n                    const $els = self.$body.find(snippetClasses).not('[data-name]').add($(snippetClasses)).add(el);\n                    $els.attr('data-name', snippet.displayName).data('name', snippet.displayName);\n                });\n\n                if (snippetEl.dataset.moduleId) {\n                    snippet.moduleId = snippetEl.dataset.moduleId;\n                    snippet.installable = true;\n                }\n\n                if (snippet.isCustom) {\n                    snippet.renameTitle = _t(\"Rename %s\", snippet.displayName);\n                    snippet.deleteTitle = _t(\"Delete %s\", snippet.displayName);\n                    snippet.isRenaming = false;\n                }\n\n                this.snippets.set(snippet.key, snippet);\n            }\n            const snippets = Array.from(this.snippets.values());\n            const customSnippets = snippets.filter(snippet => {\n                return snippet.category.id === \"snippet_custom\";\n            });\n            for (const customSnippet of customSnippets) {\n                // The \"s_button\" has a numeric value added to its name when it\n                // is custom, so we need to consider this in the search.\n                const customSnippetName = /s_button_\\d+/.test(customSnippet.name) ?\n                    \"s_button\" :\n                    customSnippet.name;\n\n                const categoryIds = snippets\n                    .filter(snippet => snippet.name === customSnippetName)\n                    .map(snippet => snippet.category.id);\n\n                customSnippet.fromCategory = categoryIds.includes(\"snippet_structure\")\n                        && categoryIds.includes(\"snippet_content\") ?\n                    \"hybrid\" : categoryIds.includes(\"snippet_structure\") ?\n                    \"structure\" : categoryIds.includes(\"snippet_content\") ?\n                    \"content\" : \"\";\n                if (customSnippet.fromCategory === \"content\") {\n                    customSnippet.group = \"\";\n                }\n            }\n        }\n        // Register the text nodes that needs to be auto-selected on click\n        this._registerDefaultTexts();\n\n        // Make elements draggable\n        this._makeSnippetDraggable();\n        this._disableUndroppableSnippets();\n\n    }\n    /**\n     * Creates a snippet editor to associated to the given snippet. If the given\n     * snippet already has a linked snippet editor, the function only returns\n     * that one.\n     * The function also instantiates a snippet editor for all snippet parents\n     * as a snippet editor must be able to display the parent snippet options.\n     *\n     * @private\n     * @param {jQuery} $snippet\n     * @param {Boolean} [forceCreate=false] To force the editor creation (e.g.,\n     * we need to create editors for invisible snippets in translate mode to be\n     * able to handle them correctly).\n     * @returns {Promise<SnippetEditor>}\n     */\n    _createSnippetEditor($snippet, forceCreate = false) {\n        var self = this;\n        var snippetEditor = $snippet.data('snippet-editor');\n        if (snippetEditor) {\n            return snippetEditor.__isStarted;\n        }\n\n        // In translate mode, only allow creating the editor if the target is a\n        // text option snippet.\n        if (!forceCreate && this.options.enableTranslation && !this._allowInTranslationMode($snippet)) {\n            return Promise.resolve(null);\n        }\n\n        var def;\n        const allowParentsEditors = this._allowParentsEditors($snippet);\n        if (allowParentsEditors) {\n            var $parent = globalSelector.closest($snippet.parent());\n            if ($parent.length) {\n                def = this._createSnippetEditor($parent);\n            }\n        }\n\n        return Promise.resolve(def).then(function (parentEditor) {\n            // When reaching this position, after the Promise resolution, the\n            // snippet editor instance might have been created by another call\n            // to _createSnippetEditor... the whole logic should be improved\n            // to avoid doing this here.\n            snippetEditor = $snippet.data('snippet-editor');\n            if (snippetEditor) {\n                return snippetEditor.__isStarted;\n            }\n\n            let editableArea = self.getEditableArea();\n            snippetEditor = new SnippetEditor(\n                parentEditor || self,\n                $snippet,\n                self.templateOptions,\n                $snippet.closest('[data-oe-type=\"html\"], .oe_structure').add(editableArea),\n                Object.assign({}, self.options, {allowParentsEditors: allowParentsEditors})\n            );\n            self.snippetEditors.push(snippetEditor);\n            // Keep parent below its child inside the DOM as its `o_handle`\n            // needs to be (visually) on top of the child ones.\n            return snippetEditor.prependTo(self.$snippetEditorArea);\n        }).then(function () {\n            return snippetEditor;\n        });\n    }\n    /**\n     * jQuery find function behavior is:\n     *\n     *     $('A').find('B C') <=> $('A B C')\n     *\n     * The searches behavior to find options' DOM needs to be:\n     *\n     *     $('A').find('B C') <=> $('A B C, B A C, AB C')\n     *\n     * This is what this function does.\n     *\n     * @todo get rid of this function and use a simple querySelectorAll, which\n     *       does not have this problem. The only issue is that jQuery selectors\n     *       support more things than querySelectorAll ones but we could stop\n     *       relying on that and/or add other filtering systems.\n     * @param {jQuery} $from - the jQuery element(s) from which to search\n     * @param {string} selector - the CSS selector to match\n     * @returns {jQuery}\n     */\n    cssFind($from, selector) {\n        // No way to correctly parse a complex jQuery selector but having no\n        // spaces should be a good-enough condition to use a simple find\n        if (selector.indexOf(' ') >= 0) {\n            return $from.closest('body').find(selector).filter((i, $el) => $from.has($el).length);\n        }\n        return $from.find(selector);\n    }\n    /**\n     * There may be no location where some snippets might be dropped. This mades\n     * them appear disabled in the menu.\n     *\n     * @todo make them undraggable\n     * @private\n     */\n    _disableUndroppableSnippets() {\n        var self = this;\n        var cache = {};\n        [...this.snippets.values()].filter(snippet => !snippet.snippetGroup)\n                .forEach((snippet) => {\n            const $snippetBody = $(snippet.baseBody);\n            const isSnippetStructure = snippet.category.id === \"snippet_structure\";\n            const isSanitizeForbidden = snippet.data.oeForbidSanitize;\n            const checkSanitize = isSanitizeForbidden === \"form\"\n                ? (el) => !el.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"])')\n                : isSanitizeForbidden\n                    ? (el) => !el.closest('[data-oe-sanitize]')\n                    : () => true;\n            const isVisible = (el) => el.closest(\".o_snippet_invisible\")\n                ? !el.closest(\"[data-invisible]\")\n                : true;\n            const canDrop = ($els) => [...$els].some((el) => checkSanitize(el) && isVisible(el));\n\n            var check = false;\n            self.templateOptions.forEach((option, k) => {\n                if (check || !($snippetBody.is(option.base_selector) && !$snippetBody.is(option.base_exclude))) {\n                    return;\n                }\n\n                k = isSanitizeForbidden ? 'forbidden/' + k : k;\n                cache[k] = cache[k] || {\n                    'drop-near': option['drop-near'] ? canDrop(option['drop-near'].all()) : false,\n                    'drop-in': option['drop-in'] ? canDrop(option['drop-in'].all()) : false,\n                };\n\n                const hasDropNear = cache[k]['drop-near'];\n                const hasDropIn = cache[k]['drop-in'];\n                // The \"isSnippetStructure\" check is useful to prevent the use\n                // of groups for hybrid snippets (\"s_form\", \"s_countdown\", etc.)\n                // Without this check, for instance, the \"Content\" snippet group\n                // would be enabled on the login page and raise a traceback if\n                // clicked.\n                check = isSnippetStructure ? hasDropIn : hasDropNear || hasDropIn;\n            });\n\n            snippet.disabled = !check;\n        });\n        // Disable snippet groups that contain no enabled snippets.\n        [...this.snippets.values()].filter(snippetGroup => snippetGroup.snippetGroup)\n                .forEach((snippetGroup) => {\n            snippetGroup.disabled = ![...self.snippets.values()].some(snippet =>\n                !snippet.disabled && (snippet.group === snippetGroup.snippetGroup)\n            )\n        });\n    }\n    /**\n     * @private\n     * @param {Object} [options={}]\n     * @returns {Object}\n     */\n    _getDragAndDropOptions(options = {}) {\n        let iframeWindow = false;\n        if (this.$body[0].ownerDocument.defaultView !== window) {\n            iframeWindow = this.$body[0].ownerDocument.defaultView;\n        }\n        return Object.assign({}, options, {\n            iframeWindow,\n            cursor: \"move\",\n        });\n    }\n    /**\n     * Creates a dropzone element and inserts it by replacing the given jQuery\n     * location. This allows to add data on the dropzone depending on the hook\n     * environment.\n     *\n     * @private\n     * @param {jQuery} $hook\n     * @param {boolean} [vertical=false]\n     * @param {Object} [style]\n     * @param {string or boolean} canBeSanitizedUnless\n     *    true: always allow\n     *    'form': allow if forms are allowed\n     *    false: always fobid\n     */\n    _insertDropzone($hook, vertical, style, canBeSanitizedUnless) {\n        const skip = $hook.closest('[data-oe-sanitize=\"no_block\"]').length;\n        let forbidSanitize;\n        if (canBeSanitizedUnless === 'form') {\n            forbidSanitize = $hook.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"]):not([data-oe-sanitize=\"no_block\"])').length;\n        } else {\n            forbidSanitize = !canBeSanitizedUnless && $hook.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"no_block\"])').length;\n        }\n        var $dropzone = $('<div/>', {\n            'class': skip ? 'd-none' : 'oe_drop_zone oe_insert' + (vertical ? ' oe_vertical' : '') +\n                (forbidSanitize ? ' text-center oe_drop_zone_danger' : ''),\n        });\n        if (style) {\n            $dropzone.css(style);\n        }\n        if (forbidSanitize) {\n            $dropzone[0].appendChild(document.createTextNode(\n                _t(\"For technical reasons, this block cannot be dropped here\")\n            ));\n        }\n        $hook.replaceWith($dropzone);\n        return $dropzone;\n    }\n    /**\n     * Creates a dropzone taking the entire area of the row in grid mode in\n     * which it will be added. It allows to place elements dragged over it\n     * inside the grid it belongs to.\n     *\n     * @param {Element} rowEl\n     */\n    _insertGridDropzone(rowEl) {\n        const columnCount = 12;\n        const rowCount = parseInt(rowEl.dataset.rowCount);\n        let $dropzone = $('<div/>', {\n            'class': 'oe_drop_zone oe_insert oe_grid_zone',\n            'style': 'grid-area: ' + 1 + '/' + 1 + '/' + (rowCount + 1) + '/' + (columnCount + 1),\n        });\n        $dropzone[0].style.minHeight = window.getComputedStyle(rowEl).height;\n        $dropzone[0].style.width = window.getComputedStyle(rowEl).width;\n        rowEl.append($dropzone[0]);\n    }\n    /**\n     * Make given snippets be draggable/droppable thanks to their thumbnail.\n     *\n     * @private\n     */\n    _makeSnippetDraggable() {\n        if (this.draggableComponent) {\n            this.draggableComponent.destroy();\n        }\n        var $toInsert, dropped, $snippet;\n        let $dropZones;\n        let isSnippetGroup;\n\n        let dragAndDropResolve;\n        let $scrollingElement = $().getScrollingElement(this.$body[0].ownerDocument);\n        if (!$scrollingElement[0]) {\n            $scrollingElement = $(this.ownerDocument).find('.o_editable');\n        }\n        const oNotebook = this.ownerDocument.querySelector(\".o_notebook\");\n        if (oNotebook) {\n            $scrollingElement = $(oNotebook);\n        }\n\n        const dragAndDropOptions = this.options.getDragAndDropOptions({\n            el: this.snippetsAreaRef.el,\n            elements: \".oe_snippet.o_we_draggable\",\n            scrollingElement: $scrollingElement[0],\n            handle: '.oe_snippet_thumbnail:not(.o_we_ongoing_insertion)',\n            cancel: '.oe_snippet.o_disabled',\n            dropzones: () => {\n                return $dropZones.toArray();\n            },\n            helper: ({ element, elementRect, helperOffset, x, y }) => {\n                const dragSnip = element.cloneNode(true);\n                dragSnip.querySelectorAll('.o_delete_btn, .o_rename_btn').forEach(\n                    el => el.remove()\n                );\n                dragSnip.style.position = \"fixed\";\n                this.$el[0].ownerDocument.body.append(dragSnip);\n                // Prepare the offset of the helper to be centered on the thumbnail image.\n                const thumbnailImgEl = element.querySelector(\".oe_snippet_thumbnail_img\");\n                helperOffset.x = thumbnailImgEl.offsetWidth / 2;\n                helperOffset.y = thumbnailImgEl.offsetHeight / 2;\n                return dragSnip;\n            },\n            onDragStart: ({ element }) => {\n                this._hideSnippetTooltips();\n\n                const prom = new Promise(resolve => dragAndDropResolve = () => resolve());\n                this._mutex.exec(() => prom);\n\n                const doc = this.options.wysiwyg.odooEditor.document;\n                $(doc.body).addClass('oe_dropzone_active');\n\n                this.options.wysiwyg.odooEditor.automaticStepUnactive();\n\n                this.$el.find('.oe_snippet_thumbnail').addClass('o_we_ongoing_insertion');\n                this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet');\n\n                dropped = false;\n                const snippetKey = element.closest('.oe_snippet').dataset.snippetKey;\n                const snippet = this.snippets.get(parseInt(snippetKey));\n                $snippet = $(element).closest('.oe_snippet');\n\n                const $baseBody = $(snippet.baseBody);\n                const { $selectorSiblings, $selectorChildren } = this._getSelectors($baseBody);\n\n                $toInsert = $baseBody.clone();\n                isSnippetGroup = $toInsert[0].matches(\".s_snippet_group\");\n                // Color-customize dynamic SVGs in dropped snippets with current theme colors.\n                [...$toInsert.find('img[src^=\"/html_editor/shape/\"], img[src^=\"/web_editor/shape/\"]')].forEach(dynamicSvg => {\n                    const colorCustomizedURL = new URL(dynamicSvg.getAttribute('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                    dynamicSvg.src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n                });\n\n                if (!$selectorSiblings.length && !$selectorChildren.length) {\n                    console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + \" have not insert action: data-drop-near or data-drop-in\");\n                    return;\n                }\n\n                const forbidSanitize = snippet.data.oeForbidSanitize;\n                const canBeSanitizedUnless = forbidSanitize === 'form' ? 'form' : !forbidSanitize;\n                // Specific case for inline snippet (e.g. \"s_badge\")\n                // Add the Snippet to the page to quickly compute its display\n                // properties (e.g. inline or not)\n                this.$body[0].appendChild($toInsert[0]);\n                $toInsert[0].classList.remove(\"oe_snippet_body\");\n                const toInsertInline = window.getComputedStyle($toInsert[0]).display.includes('inline');\n                if (!isSnippetGroup) {\n                    $toInsert[0].classList.add(\"oe_snippet_body\");\n                }\n                $toInsert[0].remove();\n\n                this._activateInsertionZones($selectorSiblings, $selectorChildren, canBeSanitizedUnless, toInsertInline);\n                $dropZones = this.getEditableArea().find('.oe_drop_zone');\n                if (forbidSanitize === 'form') {\n                    $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"]) .oe_drop_zone'));\n                } else if (forbidSanitize) {\n                    $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize] .oe_drop_zone'));\n                }\n                // If a modal is open, the scroll target must be that modal\n                const $openModal = this.getEditableArea().find('.modal:visible');\n                if ($openModal.length) {\n                    this.draggableComponent.update({ scrollingElement: $openModal[0]});\n                    $scrollingElement = $openModal;\n                }\n                this._onDropZoneStart();\n            },\n            dropzoneOver: ({ dropzone }) => {\n                if (isSnippetGroup) {\n                    dropzone.el.classList.add(\"o_dropzone_highlighted\");\n                    return;\n                }\n                if (dropped) {\n                    $toInsert.detach();\n                    $toInsert.addClass('oe_snippet_body');\n                    [...$dropZones].forEach(dropzoneEl =>\n                        dropzoneEl.classList.remove(\"invisible\"));\n                }\n                dropped = true;\n                $(dropzone.el).first().after($toInsert).addClass('invisible');\n                $toInsert.removeClass('oe_snippet_body');\n                this._onDropZoneOver();\n            },\n            dropzoneOut: ({ dropzone }) => {\n                if (isSnippetGroup) {\n                    dropzone.el.classList.remove(\"o_dropzone_highlighted\");\n                    return;\n                }\n                var prev = $toInsert.prev();\n                if (dropzone.el === prev[0]) {\n                    dropped = false;\n                    $toInsert.detach();\n                    $(dropzone.el).removeClass('invisible');\n                    $toInsert.addClass('oe_snippet_body');\n                }\n                this._onDropZoneOut();\n            },\n            onDragEnd: async ({ x, y, helper }) => {\n                const doc = this.options.wysiwyg.odooEditor.document;\n                $(doc.body).removeClass('oe_dropzone_active');\n                this.options.wysiwyg.odooEditor.automaticStepUnactive();\n                this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n                $toInsert.removeClass('oe_snippet_body');\n                $scrollingElement.off('scroll.scrolling_element');\n                if (isSnippetGroup) {\n                    const highlightedDropzoneEl = doc.body.querySelector(\".o_dropzone_highlighted\");\n                    if (highlightedDropzoneEl) {\n                        highlightedDropzoneEl.insertAdjacentElement('afterend', $toInsert[0]);\n                        dropped = true;\n                    }\n                }\n                if (!dropped && y > 3 && x + helper.getBoundingClientRect().height < this.el.getBoundingClientRect().left) {\n                    const point = { x, y };\n                    let droppedOnNotNearest = touching(doc.body.querySelectorAll('.oe_structure_not_nearest'), point);\n                    // If dropped outside of a dropzone with class oe_structure_not_nearest,\n                    // move the snippet to the nearest dropzone without it\n                    const selector = droppedOnNotNearest\n                        ? '.oe_drop_zone'\n                        : ':not(.oe_structure_not_nearest) > .oe_drop_zone';\n                    let $el = $(closest(doc.body.querySelectorAll(selector), point));\n                    // Some drop zones might have been disabled.\n                    $el = $el.filter($dropZones);\n                    if ($el.length) {\n                        $el.after($toInsert);\n                        dropped = true;\n                    }\n                }\n\n                this.getEditableArea().find('.oe_drop_zone').remove();\n\n                let $toInsertParent;\n                let prev;\n                let next;\n                if (dropped) {\n                    prev = $toInsert.first()[0].previousSibling;\n                    next = $toInsert.last()[0].nextSibling;\n\n                    $toInsertParent = $toInsert.parent();\n                    $toInsert.detach();\n                }\n\n                this.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet');\n\n                if (dropped) {\n                    if (prev) {\n                        $toInsert.insertAfter(prev);\n                    } else if (next) {\n                        $toInsert.insertBefore(next);\n                    } else {\n                        $toInsertParent.prepend($toInsert);\n                    }\n\n                    var $target = $toInsert;\n\n                    this._updateDroppedSnippet($target);\n\n                    const isSnippetGroup = $target[0].matches(\".s_snippet_group\");\n                    if (!isSnippetGroup) {\n                        this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet');\n                        await this._scrollToSnippet($target, this.$scrollable);\n                        this.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet');\n                        browser.setTimeout(async () => {\n                            // Free the mutex now to allow following operations\n                            // (mutexed as well).\n                            dragAndDropResolve();\n\n                            await this.callPostSnippetDrop($target, () => {\n                                // Restore editor to its normal edition state, also\n                                // make sure the undroppable snippets are updated.\n                                this._disableUndroppableSnippets();\n                                this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n                                this.options.wysiwyg.odooEditor.historyStep();\n                                this.$el.find('.oe_snippet_thumbnail').removeClass('o_we_ongoing_insertion');\n                            });\n                        });\n                    } else {\n                        dragAndDropResolve();\n                        this._openAddSnippetDialog($target[0].dataset.snippetGroup, $target[0]);\n                    }\n                } else {\n                    $toInsert.remove();\n                    if (dragAndDropResolve) {\n                        dragAndDropResolve();\n                    }\n                    this.$el.find('.oe_snippet_thumbnail').removeClass('o_we_ongoing_insertion');\n                }\n                this._onDropZoneStop();\n            },\n        });\n        this.draggableComponent = useDragAndDrop({ ref: { el: this.el }, ...dragAndDropOptions });\n    }\n    /**\n     * Gets the selectors that determine where the snippet can be placed.\n     *\n     * @private\n     * @param {jQuery} $baseBody\n     * @return {Object} selectors\n     */\n    _getSelectors($baseBody) {\n        let $selectorSiblings = $();\n        let $selectorChildren = $();\n        const selectorExcludeAncestor = [];\n        var temp = this.templateOptions;\n        for (const k in temp) {\n            if ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) {\n                if (temp[k]['drop-near']) {\n                    $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());\n                }\n                if (temp[k]['drop-in']) {\n                    $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());\n                }\n                if (temp[k]['drop-exclude-ancestor']) {\n                    selectorExcludeAncestor.push(temp[k]['drop-exclude-ancestor']);\n                }\n            }\n        }\n\n        // Prevent dropping an element into another one.\n        // (E.g. ToC inside another ToC)\n        for (const excludedAncestorSelector of selectorExcludeAncestor) {\n            $selectorSiblings = $selectorSiblings.filter((i, el) => !el.closest(excludedAncestorSelector));\n            $selectorChildren = $selectorChildren.filter((i, el) => !el.closest(excludedAncestorSelector));\n        }\n\n        return {\n            $selectorSiblings: $selectorSiblings,\n            $selectorChildren: $selectorChildren,\n        };\n    }\n    /**\n     * Adds the 'o_default_snippet_text' class on nodes which contain only\n     * non-empty text nodes. Those nodes are then auto-selected by the editor\n     * when they are clicked.\n     *\n     * @private\n     * @param {jQuery} [$in] - the element in which to search, default to the\n     *                       snippet bodies in the menu\n     */\n    _registerDefaultTexts($in) {\n        if ($in === undefined) {\n            // By default, we don't want the `o_default_snippet_text` class on\n            // custom snippets. Those are most likely already ready, we don't\n            // really need the auto-selection by the editor.\n            const snippets = [...this.snippets.values()]\n                .filter((snippet) => !snippet.isCustom)\n                .map((snippet) => snippet.baseBody);\n            $in = $(snippets);\n        }\n\n        $in.find('*').addBack()\n            .contents()\n            .filter(function () {\n                return this.nodeType === 3 && this.textContent.match(/\\S/);\n            }).parent().addClass('o_default_snippet_text');\n    }\n    /**\n     * Changes the content of the left panel and selects a tab.\n     *\n     * @private\n     * @param {htmlString | Element | Text | Array | jQuery} [content]\n     * the new content of the customizePanel\n     * @param {this.tabs.VALUE} [tab='blocks'] - the tab to select\n     */\n    _updateRightPanelContent({content, tab, ...options}) {\n        this._hideTooltips();\n        this._closeWidgets();\n\n        // In translation mode, only the options tab is available.\n        if (this.options.enableTranslation) {\n            tab = SnippetsMenu.tabs.OPTIONS;\n        }\n\n        this.state.currentTab = tab || SnippetsMenu.tabs.BLOCKS;\n\n        if (this._$toolbarContainer) {\n            this._$toolbarContainer[0].remove();\n        }\n        this.state.showToolbar = false;\n        this._$toolbarContainer = null;\n        if (content) {\n            // The toolbar component will be hidden or shown by state.showToolbar\n            // as it is an OWL Component, OWL is in charge of the HTML for that\n            // component. So we do not want to remove it.\n            // TODO: This should be improved when SnippetEditor / SnippetOptions\n            // are converted to OWL.\n            while (this.customizePanel.firstChild?.id !== \"o_we_editor_toolbar_container\") {\n                this.customizePanel.removeChild(this.customizePanel.firstChild);\n            }\n            $(this.customizePanel).prepend(content);\n            if (this.state.currentTab === this.tabs.OPTIONS && !options.forceEmptyTab) {\n                this._addToolbar();\n            }\n        }\n        if (options.forceEmptyTab) {\n            this.state.showToolbar = false;\n        }\n    }\n    /**\n     * Scrolls to given snippet.\n     *\n     * @private\n     * @param {jQuery} $el - snippet to scroll to\n     * @param {jQuery} [$scrollable] - $element to scroll\n     * @return {Promise}\n     */\n    async _scrollToSnippet($el, $scrollable) {\n        // Don't scroll if $el is added to a visible popup that does not fill\n        // the page (otherwise the page would scroll to a random location).\n        const modalEl = $el[0].closest('.modal');\n        if (modalEl && !$(modalEl).hasScrollableContent()) {\n            return;\n        }\n        const scrollable = $scrollable?.get(0);\n        return scrollTo($el[0], {extraOffset: 50, scrollable: scrollable});\n    }\n    /**\n     * @private\n     * @returns {HTMLElement}\n     */\n    _createLoadingElement() {\n        const loaderContainer = document.createElement('div');\n        const loader = document.createElement('img');\n        const loaderContainerClassList = [\n            'o_we_ui_loading',\n            'd-flex',\n            'justify-content-center',\n            'align-items-center',\n        ];\n        loaderContainer.classList.add(...loaderContainerClassList);\n        loader.setAttribute('src', '/web/static/img/spin.svg');\n        loaderContainer.appendChild(loader);\n        return loaderContainer;\n    }\n    /**\n     * Adds the action to the mutex queue and sets a loading effect over the\n     * editor to appear if the action takes too much time.\n     * As soon as the mutex is unlocked, the loading effect will be removed.\n     *\n     * @private\n     * @param {function} action\n     * @param {boolean|string} [contentLoading=true]\n     *     - true: puts the load effect on the edited page\n     *     - false: puts the load effect on the editor options\n     *     - \"both\": puts the load effect on both the page and the options\n     * @param {number} [delay=500]\n     * @returns {Promise}\n     */\n    async _execWithLoadingEffect(action, contentLoading = true, delay = 500) {\n        if (contentLoading === \"both\") {\n            contentLoading = false;\n            const actualAction = action;\n            action = () => {\n                this._execWithLoadingEffect(actualAction, true, delay);\n            };\n        }\n        const mutexExecResult = this._mutex.exec(action);\n        if (!this.loadingTimers[contentLoading]) {\n            const addLoader = () => {\n                if (this._loadingEffectDisabled || this.loadingElements[contentLoading]) {\n                    return;\n                }\n                this.loadingElements[contentLoading] = this._createLoadingElement();\n                if (contentLoading) {\n                    this.$snippetEditorArea.append(this.loadingElements[contentLoading]);\n                } else {\n                    this.el.appendChild(this.loadingElements[contentLoading]);\n                }\n            };\n            if (delay) {\n                this.loadingTimers[contentLoading] = setTimeout(addLoader, delay);\n            } else {\n                addLoader();\n            }\n            this._mutex.getUnlockedDef().then(() => {\n                // Note: we remove the loading element at the end of the\n                // execution queue *even if subsequent actions are content\n                // related or not*. This is a limitation of the loading feature,\n                // the goal is still to limit the number of elements in that\n                // queue anyway.\n                if (delay) {\n                    clearTimeout(this.loadingTimers[contentLoading]);\n                    this.loadingTimers[contentLoading] = undefined;\n                }\n\n                if (this.loadingElements[contentLoading]) {\n                    this.loadingElements[contentLoading].remove();\n                    this.loadingElements[contentLoading] = null;\n                }\n            });\n        }\n        return mutexExecResult;\n    }\n    /**\n     * Update the options pannel as being empty.\n     *\n     * TODO review the utility of that function and how to call it (it was not\n     * called inside a mutex then we had to do it... there must be better things\n     * to do).\n     *\n     * @private\n     */\n    _activateEmptyOptionsTab() {\n        this._updateRightPanelContent({\n            content: this.emptyOptionsTabContent,\n            tab: this.tabs.OPTIONS,\n            forceEmptyTab: true,\n        });\n    }\n    /**\n     * Hides the active tooltips.\n     *\n     * The BS documentation says that \"Tooltips that use delegation (which are\n     * created using the selector option) cannot be individually destroyed on\n     * descendant trigger elements\". So this function should be useful to remove\n     * the active tooltips manually.\n     * For instance, without this, clicking on \"Hide in Desktop\" on a snippet\n     * will leave the tooltip \"forever\" visible even if the \"Hide in Desktop\"\n     * button is gone.\n     *\n     * @private\n     */\n    _hideTooltips() {\n        // While functionally there is probably no way to have multiple active\n        // tooltips, it is possible that the panel contains multiple tooltip\n        // descriptions (we do not know what is in customers' own saved snippets\n        // for example). In any case, it does not hurt to technically consider\n        // the case anyway.\n        const tooltipTargetEls = this.el.querySelectorAll('[aria-describedby^=\"tooltip\"]');\n        for (const el of tooltipTargetEls) {\n            Tooltip.getInstance(el)?.hide();\n        }\n    }\n    /**\n     * Returns whether the edited content is a mobile view content.\n     *\n     * @returns {boolean}\n     */\n    _isMobile() {\n        return weUtils.isMobileView(this.$body[0]);\n    }\n    /**\n     * @private\n     */\n    _allowParentsEditors($snippet) {\n        return !this.options.enableTranslation;\n    }\n    /**\n     * When the editor panel receives a notification indicating that an option\n     * was used, the panel is in charge of asking for an UI update of the whole\n     * panel. Logically, the options are displayed so that an option above\n     * may influence the status and visibility of an option which is below;\n     * e.g.:\n     * - the user sets a badge type to 'info'\n     *      -> the badge background option (below) is shown as blue\n     * - the user adds a shadow\n     *      -> more options are shown afterwards to control it (not above)\n     *\n     * Technically we however update the whole editor panel (parent and child\n     * options) wherever the updates comes from. The only important thing is\n     * to first update the options UI then their visibility as their visibility\n     * may depend on their UI status.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async _snippetOptionUpdate() {\n        // Only update editors whose DOM target is still inside the document\n        // as a top option may have removed currently-enabled child items.\n        const editors = this._enabledEditorHierarchy.filter(editor => !!editor.$target[0].closest('body'));\n\n        await Promise.all(editors.map(editor => editor.updateOptionsUI()));\n        await Promise.all(editors.map(editor => editor.updateOptionsUIVisibility()));\n\n        // Always enable the deepest editor whose DOM target is still inside\n        // the document.\n        if (editors[0] !== this._enabledEditorHierarchy[0]) {\n            // No awaiting this as the mutex is currently locked here.\n            this._activateSnippet(editors[0].$target);\n        }\n    }\n    /**\n     * @private\n     */\n    _allowInTranslationMode($snippet) {\n        return globalSelector.is($snippet, { onlyTextOptions: true });\n    }\n    /**\n     * Allows to update the snippets to build & adapt dynamic content right\n     * after adding it to the DOM.\n     *\n     * @private\n     */\n    _updateDroppedSnippet($target) {\n        if ($target[0].classList.contains(\"o_snippet_drop_in_only\")) {\n            // If it's a \"drop in only\" snippet, after dropping\n            // it, we modify it so that it's no longer a\n            // draggable snippet but rather simple HTML code, as\n            // if the element had been created with the editor.\n            $target[0].classList.remove(\"o_snippet_drop_in_only\");\n            delete $target[0].dataset.snippet;\n            delete $target[0].dataset.name;\n        }\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Activates the right snippet and initializes its SnippetEditor.\n     *\n     * @private\n     */\n    _onClick(ev) {\n        // Clicking in the page should be ignored on save\n        if (this.options.wysiwyg.isSaving()) {\n            return;\n        }\n\n        var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;\n        if (!srcElement || this.lastElement === srcElement) {\n            return;\n        }\n        var $target = $(srcElement);\n        // Keep popover open if clicked inside it, but not on a button\n        if ($target.parents('.o_edit_menu_popover').length && !$target.parent('a').addBack('a').length) {\n            return;\n        }\n        this.lastElement = srcElement;\n        browser.setTimeout(() => {\n            this.lastElement = false;\n        });\n\n        if (!$target.closest('we-button, we-toggler, we-select, .o_we_color_preview').length) {\n            this._closeWidgets();\n        }\n        if (!$target.closest('body > *').length || $target.is('#iframe_target')) {\n            return;\n        }\n        if ($target.closest(this._notActivableElementsSelector).length) {\n            return;\n        }\n        const $oeStructure = $target.closest('.oe_structure');\n        if ($oeStructure.length && !$oeStructure.children().length && this.snippets.size > 0) {\n            // If empty oe_structure, encourage using snippets in there by\n            // making them \"wizz\" in the panel.\n            this._activateSnippet(false).then(() => {\n                this.$el.find('.oe_snippet').odooBounce();\n            });\n            return;\n        }\n        this._activateSnippet($target);\n    }\n    /**\n     * Called when a child editor asks for insertion zones to be enabled.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onActivateInsertionZones(ev) {\n        this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren, ev.data.canBeSanitizedUnless, ev.data.toInsertInline, ev.data.selectorGrids, ev.data.fromIframe);\n    }\n    /**\n     * Called when a child editor asks to deactivate the current snippet\n     * overlay.\n     *\n     * @private\n     */\n    _onActivateSnippet(ev) {\n        const prom = this._activateSnippet(ev.data.$snippet, ev.data.previewMode, ev.data.ifInactiveOptions);\n        if (ev.data.onSuccess) {\n            prom.then(() => ev.data.onSuccess());\n        }\n    }\n    /**\n     * Called when a child editor asks to operate some operation on all child\n     * snippet of a DOM element.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onCallForEachChildSnippet(ev) {\n        this._callForEachChildSnippet(ev.data.$snippet, ev.data.callback)\n            .then(() => ev.data.onSuccess());\n    }\n    /**\n     * Called when the overlay dimensions/positions should be recomputed.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onOverlaysCoverUpdate(ev) {\n        this.snippetEditors.forEach(editor => {\n            if (ev.data.overlayVisible) {\n                editor.toggleOverlayVisibility(true);\n            }\n            editor.cover();\n        });\n    }\n    /**\n     * Called when a child editor asks to clone a snippet, allows to correctly\n     * call the _onClone methods if the element's editor has one.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onCloneSnippet(ev) {\n        ev.stopPropagation();\n        const editor = await this._createSnippetEditor(ev.data.$snippet);\n        await editor.clone();\n        if (ev.data.onSuccess) {\n            ev.data.onSuccess();\n        }\n    }\n    /**\n     * Called when a child editor asks to clean the UI of a snippet.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onCleanUIRequest(ev) {\n        const targetEditors = this.snippetEditors.filter(editor => {\n            return ev.data.targetEl.contains(editor.$target[0]);\n        });\n        Promise.all(targetEditors.map(editor => editor.cleanUI())).then(() => {\n            ev.data.onSuccess();\n        });\n    }\n    /**\n     * Called when a child editor asks to deactivate the current snippet\n     * overlay.\n     *\n     * @private\n     */\n    _onDeactivateSnippet() {\n        this._activateSnippet(false);\n    }\n    /**\n    * Called when a snippet will move in the page.\n    *\n    * @private\n    */\n   _onSnippetDragAndDropStart() {\n        this.snippetEditorDragging = true;\n    }\n    /**\n     * Called when a snippet has moved in the page.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onSnippetDragAndDropStop(ev) {\n        this.snippetEditorDragging = false;\n        const visibleConditionalEls = [];\n        for (const snippetEditor of this.snippetEditors) {\n            const targetEl = snippetEditor.$target[0];\n            if (targetEl.dataset[\"visibility\"] === \"conditional\" &&\n                !targetEl.classList.contains(\"o_conditional_hidden\")) {\n                visibleConditionalEls.push(targetEl);\n            }\n        }\n        const modalEl = ev.data.$snippet[0].closest('.modal');\n        const carouselItemEl = ev.data.$snippet[0].closest('.carousel-item');\n        // If the snippet is in a modal, destroy editors only in that modal.\n        // This to prevent the modal from closing because of the cleanForSave\n        // on each editors. Same thing for 'carousel-item', otherwise all the\n        // editors of the 'carousel' are destroyed and the 'carousel' jumps to\n        // first slide.\n        await this._destroyEditors(carouselItemEl ? $(carouselItemEl) : modalEl ? $(modalEl) : null);\n        await this._activateSnippet(ev.data.$snippet);\n        // Because of _destroyEditors(), all the snippets with a conditional\n        // visibility are hidden. Show the ones that were visible before the\n        // drag and drop.\n        for (const visibleConditionalEl of visibleConditionalEls) {\n            visibleConditionalEl.classList.remove(\"o_conditional_hidden\");\n            delete visibleConditionalEl.dataset[\"invisible\"];\n        }\n        // Update the \"Invisible Elements\" panel as the order of invisible\n        // snippets could have changed on the page.\n        await this._updateInvisibleDOM();\n    }\n    /**\n     * Transforms an event coming from a touch screen into a mouse event.\n     *\n     * @private\n     * @param {Event} ev - a touch event\n     */\n    _onTouchEvent(ev) {\n        if (ev.touches.length > 1) {\n            // Ignore multi-touch events.\n            return;\n        }\n        const touch = ev.changedTouches[0];\n        const touchToMouse = {\n            touchstart: \"mousedown\",\n            touchmove: \"mousemove\",\n            touchend: \"mouseup\"\n        };\n        const simulatedEvent = new MouseEvent(touchToMouse[ev.type], {\n            screenX: touch.screenX,\n            screenY: touch.screenY,\n            clientX: touch.clientX,\n            clientY: touch.clientY,\n            button: 0, // left mouse button\n            bubbles: true,\n            cancelable: true,\n        });\n        touch.target.dispatchEvent(simulatedEvent);\n    }\n    /**\n     * Returns the droppable snippet from which a dropped snippet originates.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onFindSnippetTemplate(ev) {\n        const snippet = [...this.snippets.values()].find((snippet) => {\n            return snippet.name === ev.data.snippet.dataset.snippet;\n        });\n        if (snippet) {\n            ev.data.callback(snippet);\n        }\n    }\n    /**\n     * @private\n     */\n    _onHideOverlay() {\n        for (const editor of this.snippetEditors) {\n            editor.toggleOverlay(false);\n        }\n    }\n    /**\n     * Calls back if the specified element is selected.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onIsElementSelected(ev) {\n        for (const editor of this.snippetEditors) {\n            if (editor.isShown() && editor.$target[0] === ev.data.el) {\n                ev.data.callback();\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInstallBtnClick(ev) {\n        const snippetEl = ev.currentTarget.closest('[data-module-id]');\n        const moduleID = parseInt(snippetEl.dataset.moduleId);\n        const snippetName = snippetEl.getAttribute(\"name\");\n        this._installModule(moduleID, snippetName);\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    async onInvisibleEntryClick(invisibleEntry) {\n        const toggleVisibility = async (snippetEl) => {\n            const isVisible = await this._execWithLoadingEffect(async () => {\n                const editor = await this._createSnippetEditor($(snippetEl));\n                const show = editor.toggleTargetVisibility();\n                this._disableUndroppableSnippets();\n                return show;\n            }, true);\n            invisibleEntry.isVisible = isVisible;\n            this._activateSnippet(isVisible ? $(snippetEl) : false);\n        };\n\n        // Toggle all its descendants to invisible (Hide)\n        if (invisibleEntry.isVisible) {\n            invisibleEntry.children.forEach((child) => {\n                if (child.isVisible) {\n                    this.onInvisibleEntryClick(child);\n                }\n            });\n        } else if (invisibleEntry.parents && !invisibleEntry.parents.isVisible) {\n            // Toggle all its parents to visible (show)\n            this.onInvisibleEntryClick(invisibleEntry.parents);\n        }\n\n        await toggleVisibility(invisibleEntry.snippetEl);\n    }\n    /**\n     * @private\n     */\n    _onBlocksTabClick(ev) {\n        this._activateSnippet(false);\n    }\n    /**\n     * @private\n     */\n    _onOptionsTabClick(ev) {\n        if (!ev.currentTarget.classList.contains('active')) {\n            this._activateSnippet(false);\n            this._mutex.exec(() => {\n                this._activateEmptyOptionsTab();\n            });\n        }\n    }\n    /**\n     * @private\n     */\n    _onDeleteBtnClick(ev) {\n        ev.stopPropagation();\n        const snippetKey = parseInt(ev.currentTarget.dataset.snippetKey);\n        this._deleteCustomSnippet(snippetKey);\n    }\n    /**\n     * @private\n     */\n    _onRenameBtnClick(ev) {\n        const snippetKey = parseInt(ev.currentTarget.dataset.snippetKey);\n        const snippet = this.snippets.get(snippetKey);\n        snippet.renaming = true;\n    }\n    /**\n     * @private\n     */\n    async _onConfirmRename(ev) {\n        const input = ev.target.parentElement.querySelector(\"input\");\n        const snippetKey = parseInt(ev.target.closest(\".oe_snippet\").dataset.snippetKey);\n        const newName = input.value;\n        this._renameCustomSnippet(snippetKey, newName);\n    }\n    /**\n     * Prevents pointer-events to change the focus when a pointer slide from\n     * left-panel to the editable area.\n     *\n     * @private\n     */\n    _onMouseDown(ev) {\n        const $blockedArea = $('#wrapwrap'); // TODO should get that element another way\n        this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n        $blockedArea.addClass('o_we_no_pointer_events');\n        const reenable = () => {\n            this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n            $blockedArea.removeClass('o_we_no_pointer_events');\n        };\n        // Use a setTimeout fallback to avoid locking the editor if the mouseup\n        // is fired over an element which stops propagation for example.\n        const enableTimeoutID = setTimeout(() => reenable(), 5000);\n        $(document).one('mouseup', () => {\n            clearTimeout(enableTimeoutID);\n            reenable();\n        });\n    }\n    /**\n     * @private\n     */\n    _onMouseUp(ev) {\n        const snippetEl = ev.target.closest('.oe_snippet');\n        if (snippetEl && snippetEl.classList.contains(\"o_we_draggable\")) {\n            this._showSnippetTooltip(snippetEl);\n        }\n    }\n    /**\n     * Displays an autofading tooltip over a snippet, after a delay.\n     * If in the meantime the user has started to drag the snippet, it won't be\n     * shown.\n     *\n     * TODO: remove delay param in master\n     *\n     * @private\n     * @param {jQuery} $snippet\n     * @param {Number} [delay=1500]\n     */\n    _showSnippetTooltip(snippetEl, delay = 1500) {\n        if (snippetEl.dataset.snippetGroup || snippetEl.classList.contains(\"o_snippet_install\")) {\n            return;\n        }\n        if (this.hideShownTooltip) {\n            this.hideShownTooltip();\n        }\n        this.hideShownTooltip = this.popover.add(snippetEl, OdooTooltip, {\n            tooltip: _t(\"Drag and drop the building block.\"),\n        });\n        this._hideSnippetTooltips(1500);\n    }\n    /**\n     * @private\n     * @param {Number} [delay=0]\n     */\n    _hideSnippetTooltips(delay = 0) {\n        clearTimeout(this.__hideSnippetTooltipTimeout);\n        this.__hideSnippetTooltipTimeout = setTimeout(() => {\n            this.hideShownTooltip?.();\n        }, delay);\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onGetSnippetVersions(ev) {\n        const snippet = [...this.snippets.values()].find((snippet) => snippet.name === ev.data.snippetName);\n        ev.data.onSuccess(snippet && {\n            vcss: snippet.data.vcss,\n            vjs: snippet.data.vjs,\n            vxml: snippet.data.vxml,\n        });\n    }\n    /**\n     * UNUSED: used to be called when saving a custom snippet. We now save and\n     * reload the page when saving a custom snippet so that all the DOM cleanup\n     * mechanisms are run before saving. Kept for compatibility.\n     *\n     * TODO: remove in master / find a way to clean the DOM without save+reload\n     *\n     * @private\n     */\n    async _onReloadSnippetTemplate(ev) {\n        await this._activateSnippet(false);\n        this.invalidateSnippetCache = true;\n        await this._loadSnippetsTemplates();\n    }\n    /**\n     * @private\n     */\n    _onBlockPreviewOverlays(ev) {\n        this._blockPreviewOverlays = true;\n    }\n    /**\n     * @private\n     */\n    _onUnblockPreviewOverlays(ev) {\n        this._blockPreviewOverlays = false;\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onRemoveSnippet(ev) {\n        ev.stopPropagation();\n        const editor = await this._createSnippetEditor(ev.data.$snippet);\n        await editor.removeSnippet(ev.data.shouldRecordUndo);\n        if (ev.data.onSuccess) {\n            ev.data.onSuccess();\n        }\n    }\n    /**\n     * Saving will destroy all editors since they need to clean their DOM.\n     * This has thus to be done when they are all finished doing their work.\n     *\n     * @private\n     */\n    _onSaveRequest(ev) {\n        const data = ev.data || {};\n        if (data.invalidateSnippetCache) {\n            this.invalidateSnippetCache = true;\n        }\n        // If it's an OdooEvent sent by sub-widgets, we prevent the event\n        // from triggering the request on the parent.\n        ev.stopped = true;\n        this._buttonClick(async (after) => {\n            await this.postSnippetDropPromise;\n            return this._execWithLoadingEffect(() => {\n                const oldOnFailure = data.onFailure;\n                data.onFailure = () => {\n                    if (oldOnFailure) {\n                        oldOnFailure();\n                    }\n                    after();\n                };\n                this.props.trigger_up({\n                    name: 'request_save',\n                    data\n                });\n            }, true);\n        }, this.$el[0].querySelector('button[data-action=save]'));\n    }\n    /**\n     * @private\n     */\n    _onSnippetClick(ev) {\n        if (!ev.currentTarget.matches(\".o_disabled\") && ev.currentTarget.dataset.snippetGroup) {\n            this._openAddSnippetDialog(ev.currentTarget.dataset.snippetGroup, ev.currentTarget);\n        } else {\n            const $els = this.getEditableArea().find('.oe_structure.oe_empty').addBack('.oe_structure.oe_empty');\n            for (const el of $els) {\n                if (!el.children.length) {\n                    $(el).odooBounce('o_we_snippet_area_animation');\n                }\n            }\n        }\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     * @param {Object} ev.data\n     * @param {function} ev.data.exec\n     */\n    _onSnippetEditionRequest(ev) {\n        this._execWithLoadingEffect(ev.data.exec, ev.data.optionsLoader ? \"both\" : true);\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSnippetEditorDestroyed(ev) {\n        ev.stopPropagation();\n        const index = this.snippetEditors.indexOf(ev.target);\n        this.snippetEditors.splice(index, 1);\n    }\n    /**\n     * @private\n     */\n    _onSnippetCloned(ev) {\n        this._updateInvisibleDOM();\n    }\n    /**\n     * Called when a snippet is removed -> checks if there is draggable snippets\n     * to enable/disable as the DOM changed.\n     *\n     * @private\n     */\n    _onSnippetRemoved() {\n        this._disableUndroppableSnippets();\n        this._updateInvisibleDOM();\n    }\n    /**\n     * @see _snippetOptionUpdate\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSnippetOptionUpdate(ev) {\n        ev.stopPropagation();\n        (async () => {\n            await this._snippetOptionUpdate();\n            ev.data.onSuccess();\n        })();\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    async _onSnippetOptionVisibilityUpdate(ev) {\n        if (this.options.wysiwyg.isSaving()) {\n            // Do not update the option visibilities if we are destroying them.\n            return;\n        }\n        if (!ev.data.show) {\n            await this._activateSnippet(false);\n        }\n        await this._updateInvisibleDOM(); // Re-render to update status\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSnippetThumbnailURLRequest(ev) {\n        if (!ev.data.key) {\n            ev.data.onSuccess(\"\");\n        }\n        const snippet = [...this.snippets.values()].find((snippet) =>\n            !snippet.isCustom && snippet.name === ev.data.key\n        );\n        ev.data.onSuccess(snippet ? snippet.thumbnailSrc : '');\n    }\n    /**\n     * Called when an user value widget is being opened -> close all the other\n     * user value widgets of all editors + add backdrop.\n     */\n    _onUserValueWidgetOpening() {\n        this._closeWidgets();\n        this.el.classList.add('o_we_backdrop');\n    }\n    /**\n     * Called when an user value widget is being closed -> rely on the fact only\n     * one widget can be opened at a time: remove the backdrop.\n     */\n    _onUserValueWidgetClosing() {\n        this.el.classList.remove('o_we_backdrop');\n    }\n    /**\n     * Called when a child editor asks to update the \"Invisible Elements\" panel.\n     *\n     * @private\n     */\n    async _onUpdateInvisibleDom() {\n        await this._updateInvisibleDOM();\n    }\n    _addToolbar(toolbarMode = \"text\") {\n        // TODO: Now that the toolbar is not removed every time\n        // `_updateRightPanelContent` is called, we should probably rename this\n        // method ot \"_updateToolbar\" and remove some of the now useless code,\n        // since the only important thing is to check the visibility\n        // and rename the toolbar. The event binding happening every time is\n        // probably not necessary either.\n        if (this.folded) {\n            return;\n        }\n        let titleText = _t(\"Inline Text\");\n        switch (toolbarMode) {\n            case \"image\":\n                titleText = _t(\"Image Formatting\");\n                break;\n            case \"video\":\n                titleText = _t(\"Video Formatting\");\n                break;\n            case \"picto\":\n                titleText = _t(\"Icon Formatting\");\n                break;\n        }\n        this.state.toolbarTitle = titleText;\n        // In case, the snippetEditor is inside an iframe, rebind the dropdown\n        // from the iframe.\n        for (const dropdown of this._toolbarWrapperEl.querySelectorAll('.colorpicker-group')) {\n            const $ = dropdown.ownerDocument.defaultView.$;\n            const $dropdown = $(dropdown);\n            $dropdown.off('show.bs.dropdown');\n            $dropdown.on('show.bs.dropdown', () => {\n                this.options.wysiwyg.onColorpaletteDropdownShow(dropdown.dataset.colorType);\n            });\n            $dropdown.off('hide.bs.dropdown');\n            $dropdown.on('hide.bs.dropdown', (ev) => this.options.wysiwyg.onColorpaletteDropdownHide(ev));\n        }\n\n        this._checkEditorToolbarVisibility();\n    }\n    /**\n     * Update editor UI visibility based on the current range.\n     */\n    _checkEditorToolbarVisibility(e) {\n        const $toolbarTableContainer = this.$('#o-we-editor-table-container');\n        const selection = this.options.wysiwyg.odooEditor.document.getSelection();\n        const range = selection && selection.rangeCount && selection.getRangeAt(0);\n        const $currentSelectionTarget = $(range && range.commonAncestorContainer);\n        // Do not  toggle visibility if the target is inside the toolbar ( eg.\n        // during link edition).\n        if ($currentSelectionTarget.closest('#o_we_editor_toolbar_container').length ||\n            (e && $(e.target).closest('#o_we_editor_toolbar_container').length)\n        ) {\n            return;\n        }\n        this.state.showToolbar = !(!range ||\n            !$currentSelectionTarget.parents('#wrapwrap, .iframe-editor-wrapper').length ||\n            closestElement(selection.anchorNode, '[data-oe-model]:not([data-oe-type=\"html\"]):not([data-oe-field=\"arch\"]):not([data-oe-translation-source-sha])') ||\n            closestElement(selection.focusNode, '[data-oe-model]:not([data-oe-type=\"html\"]):not([data-oe-field=\"arch\"]):not([data-oe-translation-source-sha])') ||\n            (e && $(e.target).closest('.fa, img').length ||\n            this.options.wysiwyg.lastMediaClicked && $(this.options.wysiwyg.lastMediaClicked).is('.fa, img')) ||\n            (this.options.wysiwyg.lastElement && !this.options.wysiwyg.lastElement.isContentEditable)\n        );\n        const isInsideTD = !!(\n            range &&\n            $(range.startContainer).closest('.o_editable td').length &&\n            $(range.endContainer).closest('.o_editable td').length\n        );\n        $toolbarTableContainer.toggleClass('d-none', !isInsideTD);\n    }\n    /**\n     * On click on discard button.\n     */\n    _onDiscardClick() {\n        this._buttonClick(after => {\n            this.snippetEditors.forEach(editor => {\n                editor.toggleOverlay(false);\n            });\n            this.props.trigger_up({ name: 'request_cancel', data: {onReject: after} });\n        }, this.$el[0].querySelector('button[data-action=cancel]'), false);\n    }\n    /**\n     * Preview on mobile.\n     */\n    _onMobilePreviewClick() {\n        // TODO refactor things to make this more understandable -> on mobile\n        // edition, update the UI. But to do it properly and inside the mutex\n        // this simulates what happens when a snippet option is used.\n        this._execWithLoadingEffect(async () => {\n            const initialBodySize = this.$body[0].clientWidth;\n            this._toggleMobilePreview();\n\n            // TODO needed so that mobile edition is considered before updating\n            // the UI but this is clearly random. The trigger_up above should\n            // properly await for the rerender somehow or, better, the UI update\n            // should not depend on the mobile re-render entirely.\n            let count = 0;\n            do {\n                await new Promise(resolve => setTimeout(resolve, 1));\n            // Technically, should not be possible to fall into an infinite loop\n            // but extra safety as a stable fix.\n            } while (count++ < 1000 && Math.abs(this.$body[0].clientWidth - initialBodySize) < 1);\n\n            // Reload images inside grid items so that no image disappears when\n            // activating mobile preview.\n            const $gridItemEls = this.getEditableArea().find('div.o_grid_item');\n            for (const gridItemEl of $gridItemEls) {\n                gridUtils._reloadLazyImages(gridItemEl);\n            }\n\n            const isMobilePreview = weUtils.isMobileView(this.$body[0]);\n            for (const invisibleOverrideEl of this.getEditableArea().find('.o_snippet_mobile_invisible, .o_snippet_desktop_invisible')) {\n                const isMobileHidden = invisibleOverrideEl.classList.contains(\"o_snippet_mobile_invisible\");\n                invisibleOverrideEl.classList.remove('o_snippet_override_invisible');\n                if (isMobilePreview === isMobileHidden) {\n                    invisibleOverrideEl.dataset.invisible = '1';\n                } else {\n                    delete invisibleOverrideEl.dataset.invisible;\n                }\n            }\n\n            // This is async but using the main editor mutex, currently locked.\n            this._updateInvisibleDOM();\n\n            return this._snippetOptionUpdate();\n        }, false);\n    }\n    /**\n     * Method for modules to override if they want to integrate with the\n     * Mobile Preview feature.\n     */\n    _toggleMobilePreview() {\n        return false;\n    }\n    /**\n     * Undo..\n     */\n    async _onUndo() {\n        this.options.wysiwyg.undo();\n    }\n    /**\n     * Redo.\n     */\n    async _onRedo() {\n        this.options.wysiwyg.redo();\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onRequestEditable(ev) {\n        ev.data.callback($(this.options.wysiwyg.odooEditor.editable));\n    }\n    /**\n     * Enable loading effects\n     *\n     * @private\n     */\n    _onEnableLoadingEffect() {\n        this._loadingEffectDisabled = false;\n    }\n    /**\n     * Disable loading effects and cancel the one displayed\n     *\n     * @private\n     */\n    _onDisableLoadingEffect() {\n        this._loadingEffectDisabled = true;\n        Object.keys(this.loadingElements).forEach(key => {\n            if (this.loadingElements[key]) {\n                this.loadingElements[key].remove();\n                this.loadingElements[key] = null;\n            }\n        });\n    }\n    /***\n     * Display a loading effect on the clicked button, and disables the other\n     * buttons. Passes an argument to restore the buttons to their normal\n     * state to the function to execute.\n     *\n     * @param action {Function} The action to execute\n     * @param button {HTMLElement} The button element\n     * @param addLoadingEffect {boolean} whether or not to add a loading effect.\n     * @returns {Promise<void>}\n     * @private\n     */\n    async _buttonClick(action, button, addLoadingEffect = true) {\n        if (this._buttonAction) {\n            return;\n        }\n        this._buttonAction = true;\n        let removeLoadingEffect;\n        // Remove the tooltips now, because the button will be disabled and so,\n        // the tooltip will not be removable (see BS doc).\n        this._hideTooltips();\n        if (addLoadingEffect) {\n            removeLoadingEffect = addButtonLoadingEffect(button);\n        }\n        const actionButtons = this.$el[0].querySelectorAll('[data-action]');\n        for (const actionButton of actionButtons) {\n            actionButton.disabled = true;\n        }\n        const after = () => {\n            if (removeLoadingEffect) {\n                removeLoadingEffect();\n            }\n            for (const actionButton of actionButtons) {\n                actionButton.disabled = false;\n            }\n        };\n        await action(after);\n        this._buttonAction = false;\n    }\n    /**\n     * Allows module to enable options for elements that are not snippets.\n     * Such as website params, or mass_mailing font-size.\n     *\n     * @param {String} tab - The tab to enable (defined in this.tabs)\n     */\n    async _enableFakeOptionsTab(tab) {\n        // Note: nothing async here but start the loading effect asap\n        let releaseLoader;\n        try {\n            const promise = new Promise(resolve => releaseLoader = resolve);\n            this._execWithLoadingEffect(() => promise, false, 0);\n            // Loader is added to the DOM synchronously\n            await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));\n            // Ensure loader is rendered: first call asks for the (already done)\n            // DOM update, second call happens only after rendering the first\n            // \"updates\"\n\n            if (!this.topFakeOptionEl) {\n                let el;\n                for (const [elementName, title] of this.constructor.optionsTabStructure) {\n                    const newEl = document.createElement(elementName);\n                    newEl.dataset.name = title;\n                    if (el) {\n                        el.appendChild(newEl);\n                    } else {\n                        this.topFakeOptionEl = newEl;\n                    }\n                    el = newEl;\n                }\n                this.bottomFakeOptionEl = el;\n                this.$body[0].appendChild(this.topFakeOptionEl);\n            }\n\n            // Need all of this in that order so that:\n            // - the element is visible and can be enabled and the onFocus\n            //   method is called each time.\n            // - the element is hidden afterwards so it does not take space in\n            //   the DOM, same as the overlay which may make a scrollbar appear.\n            this.topFakeOptionEl.classList.remove('d-none');\n            const editorPromise = this._activateSnippet($(this.bottomFakeOptionEl));\n            // Because _activateSnippet uses the same mutex as the loader\n            releaseLoader();\n            releaseLoader = undefined;\n            const editor = await editorPromise;\n            this.topFakeOptionEl.classList.add('d-none');\n            editor.toggleOverlay(false);\n\n            this._updateRightPanelContent({\n                tab,\n            });\n        } catch (e) {\n            // Normally the loading effect is removed in case of error during\n            // the action but here the actual activity is happening outside of\n            // the action, the effect must therefore be cleared in case of error\n            // as well.\n            if (releaseLoader) {\n                releaseLoader();\n            }\n            throw e;\n        }\n    }\n    /**\n     * Allows other modules to react to drop zones being enabled\n     *\n     * @private\n     */\n    _onDropZoneStart() {}\n    /**\n     * @see _onDropZoneStart\n     *\n     * @private\n     */\n    _onDropZoneOver() {}\n    /**\n     * @see _onDropZoneStart\n     *\n     * @private\n     */\n    _onDropZoneOut() {}\n    /**\n     * @see _onDropZoneStart\n     *\n     * @private\n     */\n    _onDropZoneStop() {}\n    /**\n     * Compatibility layer for legacy widgets. Should be removed when everything\n     * is converted to OWL.\n     *\n     * @param ev {CustomEvent}\n     */\n    _trigger_up(ev) {\n        if (ev.name in this.constructor.custom_events) {\n            this[this.constructor.custom_events[ev.name]](ev);\n        }\n        if (!ev.stopped) {\n            return this.props.trigger_up(ev);\n        }\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onOpenAddSnippetDialog(ev) {\n        this._openAddSnippetDialog(ev.data.snippetGroup, ev.data.initialSnippetEl);\n    }\n    /**\n     * Open a dialog with previews of snippets to add to the page.\n     *\n     * @param {String} [snippetGroup=null]\n     * @param {HTMLElement} initialSnippetEl\n     * @private\n     */\n    async _openAddSnippetDialog(snippetGroup = null, initialSnippetEl) {\n        this._mutex.exec(async () => {\n            let hookEl = null;\n            let dropZoneEls = null;\n            let isSnippetChosen = false;\n            const snippetThumbnails = this.el.querySelectorAll(\".oe_snippet_thumbnail\");\n            const isSnippetGroupClicked = initialSnippetEl.matches(\".oe_snippet[data-snippet-group]\");\n            const groupSelected = snippetGroup ||\n                [...this.snippets.values()].find(snippet =>\n                    snippet.name === initialSnippetEl.dataset.snippet\n                ).group;\n\n            this.options.wysiwyg.odooEditor.historyPauseSteps();\n            if (isSnippetGroupClicked) {\n                const thumbnailEl = initialSnippetEl.querySelector(\".oe_snippet_thumbnail\");\n                thumbnailEl.classList.add(\"o_we_ongoing_insertion\");\n                // When the \"snippet group block\" is clicked, we add drop zones on\n                // the page where the snippet can be placed, then we detect\n                // the drop zone closest to the middle of the page.\n                const selectors = this._getSelectors($(\"<section></section>\"));\n                this._activateInsertionZones(selectors.$selectorSiblings, selectors.$selectorChildren, false, false);\n                dropZoneEls = this.$body[0].querySelectorAll(\".oe_drop_zone\");\n                dropZoneEls.forEach(dropZoneEl => dropZoneEl.classList.add(\"invisible\"));\n                // Do not allow drop by click in another snippet\n                // (e.g., \"table of content\") unless it is a \"s_popup\".\n                dropZoneEls = [...dropZoneEls].filter(dropzoneEl => {\n                    return !dropzoneEl.closest(\"[data-snippet]:not(.s_popup), #website_cookies_bar\");\n                });\n                hookEl = this._getClosestDropzone(dropZoneEls)\n                    || dropZoneEls[dropZoneEls.length - 1];\n                hookEl.classList.add(\"o_hook_drop_zone\");\n            } else {\n                hookEl = initialSnippetEl;\n            }\n\n            const hookParentEl = hookEl.parentNode;\n            // Excludes snippets that cannot be placed at the target location.\n            [...this.snippets.values()].forEach((snippet) => {\n                if (snippet.disabled) {\n                    snippet.excluded = true;\n                } else {\n                    const $snippetSelectorChildren =\n                            this._getSelectors($(snippet.baseBody)).$selectorChildren;\n                    const hasSelectorChild = [...$snippetSelectorChildren].some(snippetSelectorChild => {\n                        return snippetSelectorChild === hookParentEl;\n                    });\n                    const forbidSanitize = snippet.data.oeForbidSanitize;\n                    let isForbidden = false;\n                    if (forbidSanitize === \"form\") {\n                        isForbidden = hookEl.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"])');\n                    } else if (forbidSanitize) {\n                        isForbidden = hookEl.closest(\"[data-oe-sanitize]\");\n                    }\n                    snippet.excluded = !hasSelectorChild || isForbidden;\n                }\n            });\n\n            const hasIncludedSnippet = [...this.snippets.values()].some(snippet => snippet.excluded === false);\n            if (!hasIncludedSnippet) {\n                if (dropZoneEls) {\n                    dropZoneEls.forEach(dropZoneEl => dropZoneEl.remove());\n                }\n                this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n                for (const snippetThumbnail of snippetThumbnails) {\n                    snippetThumbnail.classList.remove('o_we_ongoing_insertion');\n                }\n                return;\n            }\n\n            await new Promise(resolve => {\n                this.dialog.add(AddSnippetDialog, {\n                    snippets: this.snippets,\n                    groupSelected: groupSelected,\n                    optionsSnippets: this.options.snippets,\n                    frontendDirection: this.options.direction,\n                    installModule: (moduleID, snippetName) => {\n                        resolve();\n                        this._installModule(moduleID, snippetName);\n                    },\n                    addSnippet: async (snippetEl) => {\n                        isSnippetChosen = true;\n                        // Depending on 3 possible scenarios, \"hookEl\" can be:\n                        // - The \"s_snippet_group\" template => When a snippet group is\n                        // dropped from the side panel into the page.\n                        // - The closest drop zone to the page center => When a snippet\n                        // group is clicked in the side panel.\n                        // - The snippet to be replaced => When the \"replace\" overlay\n                        // button is clicked.\n                        hookEl.parentNode.insertBefore(snippetEl, hookEl);\n                        hookEl.parentNode.removeChild(hookEl);\n                        this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n                        await this._scrollToSnippet($(snippetEl), this.$scrollable);\n                        this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n                        browser.setTimeout(async () => {\n                            resolve();\n                            await this.callPostSnippetDrop($(snippetEl), () => {\n                                // Restore editor to its normal edition state, also\n                                // make sure the undroppable snippets are updated.\n                                this._disableUndroppableSnippets();\n                                this.options.wysiwyg.odooEditor.historyStep();\n                                for (const snippetThumbnail of snippetThumbnails) {\n                                    snippetThumbnail.classList.remove('o_we_ongoing_insertion');\n                                }\n                            });\n                        });\n                    },\n                    deleteCustomSnippet: (snippetKey) => {\n                        return this._deleteCustomSnippet(snippetKey, false);\n                    },\n                    renameCustomSnippet: (snippetKey, newName) => {\n                        this._renameCustomSnippet(snippetKey, newName, false);\n                    },\n                }, {\n                    onClose: () => {\n                        if (isSnippetGroupClicked) {\n                            dropZoneEls = this.$body[0].querySelectorAll(\".oe_drop_zone\")\n                            if (dropZoneEls) {\n                                dropZoneEls.forEach(dropZoneEl => dropZoneEl.remove());\n                            }\n                        } else if (!isSnippetChosen && snippetGroup) {\n                            initialSnippetEl.remove();\n                        }\n                        if (!isSnippetChosen) {\n                            this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n                            this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n                            for (const snippetThumbnail of snippetThumbnails) {\n                                snippetThumbnail.classList.remove('o_we_ongoing_insertion');\n                            }\n                            resolve();\n                        }\n                    }\n                });\n            });\n        });\n    }\n    /**\n     * Gets the dropzone closest to the center of the viewport, excluding\n     * dropzones located in the top quarter of the viewport.\n     *\n     * @private\n     * @param {HTMLCollection} dropZoneEls\n     * @return {element} closestDropZoneEl\n     */\n    _getClosestDropzone(dropZoneEls) {\n        let closestDropZoneEl = null;\n        let closestDistance = Infinity;\n        const iframeWindow = this.$body[0].ownerDocument.defaultView;\n        const iframeWindowMidY = iframeWindow.innerHeight / 2;\n\n        for (const dropZoneEl of dropZoneEls) {\n            const rect = dropZoneEl.getBoundingClientRect();\n            if (0 > (rect.top - (iframeWindowMidY / 2))) {\n                continue;\n            }\n            const dropZoneElMidY = rect.top + (rect.height / 2);\n            const distance = Math.abs(iframeWindowMidY - dropZoneElMidY);\n\n            if (distance < closestDistance) {\n                closestDistance = distance;\n                closestDropZoneEl = dropZoneEl;\n            }\n        };\n\n        return closestDropZoneEl;\n    }\n    /**\n     * Installs the module of the selected snippet.\n     *\n     * @private\n     * @param {Number} moduleID\n     * @param {String} snippetName\n     */\n    _installModule(moduleID, snippetName) {\n        // TODO: Should be the app name, not the snippet name ... Maybe both ?\n        const bodyText = _t(\"Do you want to install %s App?\", snippetName);\n        const linkText = _t(\"More info about this app.\");\n        const linkUrl = '/odoo/action-base.open_module_tree/' + encodeURIComponent(moduleID);\n        this.dialog.add(ConfirmationDialog, {\n            title: _t(\"Install %s\", snippetName),\n            body: markup(`${escape(bodyText)}\\n<a href=\"${linkUrl}\" target=\"_blank\">${escape(linkText)}</a>`),\n            confirm: async () => {\n                try {\n                    await this.orm.call(\"ir.module.module\", \"button_immediate_install\", [[moduleID]]);\n                    this.invalidateSnippetCache = true;\n                    this._onSaveRequest({\n                        data: {\n                            reloadWebClient: true,\n                        }\n                    });\n                } catch (e) {\n                    if (e instanceof RPCError) {\n                        const message = escape(_t(\"Could not install module %s\", snippetName));\n                        this.notification.add(message, {\n                            type: \"danger\",\n                            sticky: true,\n                        });\n                    } else {\n                        throw e;\n                    }\n                }\n            },\n            confirmLabel: _t(\"Save and Install\"),\n            cancel: () => {},\n        });\n    }\n    /**\n     * Deletes a custom snippet.\n     *\n     * @private\n     * @param {Number} snippetKey\n     */\n    async _deleteCustomSnippet(snippetKey, withMutex = true) {\n        const snippet = this.snippets.get(snippetKey);\n        const message = _t(\"Are you sure you want to delete the block %s?\", snippet.displayName);\n        return new Promise(resolve => {\n            this.dialog.add(ConfirmationDialog, {\n                body: message,\n                confirm: async () => {\n                    await this.orm.call(\"ir.ui.view\", \"delete_snippet\", [], {\n                        'view_id': snippet.id,\n                        'template_key': this.options.snippets,\n                    });\n                    this.invalidateSnippetCache = true;\n                    this.snippets.delete(snippetKey);\n                    await this._loadSnippetsTemplates(withMutex);\n                    resolve();\n                },\n                cancel: () => {\n                    resolve();\n                    return null;\n                },\n                confirmLabel: _t(\"Yes\"),\n                cancelLabel: _t(\"No\"),\n            });\n        });\n    }\n    /**\n     * Renames a custom snippet.\n     *\n     * @private\n     * @param {Number} snippetKey\n     * @param {String} newName\n     */\n    async _renameCustomSnippet(snippetKey, newName, withMutex = true) {\n        const snippet = this.snippets.get(snippetKey);\n        if (newName !== snippet.displayName) {\n            await this.orm.call(\"ir.ui.view\", \"rename_snippet\", [], {\n                'name': newName,\n                'view_id': snippet.id,\n                'template_key': this.options.snippets,\n            });\n            // Prevent the name flashing while reloading the template.\n            this.invalidateSnippetCache = newName !== snippet.displayName;\n            snippet.displayName = newName;\n            await this._loadSnippetsTemplates(withMutex);\n        }\n        snippet.renaming = false;\n    }\n}\n\nexport default {\n    SnippetsMenu: SnippetsMenu,\n    SnippetEditor: SnippetEditor,\n    globalSelector: globalSelector,\n};\n", "/** @odoo-module **/\n\nimport { attachComponent } from \"@web_editor/js/core/owl_utils\";\nimport { MediaDialog } from \"@web_editor/components/media_dialog/media_dialog\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { throttleForAnimation, debounce } from \"@web/core/utils/timing\";\nimport { clamp } from \"@web/core/utils/numbers\";\nimport { scrollTo } from \"@web_editor/js/common/scrolling\";\nimport publicWidget from \"@web/legacy/js/public/public_widget\";\nimport { ColorPalette } from \"@web_editor/js/wysiwyg/widgets/color_palette\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport * as gridUtils from \"@web_editor/js/common/grid_layout_utils\";\nimport {ColumnLayoutMixin} from \"@web_editor/js/common/column_layout_mixin\";\nconst {\n    normalizeColor,\n    getBgImageURL,\n    backgroundImageCssToParts,\n    backgroundImagePartsToCss,\n    DEFAULT_PALETTE,\n    isBackgroundImageAttribute,\n} = weUtils;\nimport { ImageCrop } from '@web_editor/js/wysiwyg/widgets/image_crop';\nimport {\n    loadImage,\n    loadImageInfo,\n    applyModifications,\n    removeOnImageChangeAttrs,\n    isImageSupportedForProcessing,\n    isImageSupportedForStyle,\n    createDataURL,\n    isGif,\n    getDataURLBinarySize,\n} from \"@web_editor/js/editor/image_processing\";\nimport * as OdooEditorLib from \"@web_editor/js/editor/odoo-editor/src/OdooEditor\";\nimport { pick } from \"@web/core/utils/objects\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport {\n    isCSSColor,\n    convertCSSColorToRgba,\n    normalizeCSSColor,\n } from '@web/core/utils/colors';\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst preserveCursor = OdooEditorLib.preserveCursor;\nconst { DateTime } = luxon;\nconst resetOuids = OdooEditorLib.resetOuids;\nlet _serviceCache = {\n    orm: {},\n    rpc: {},\n};\nconst clearServiceCache = () => {\n    _serviceCache = {\n        orm: {},\n        rpc: {},\n    };\n};\n\n// Regex definitions to apply speed modification in SVG files\n// Note : These regex patterns are duplicated on the server side for\n// background images that are part of a CSS rule \"background-image: ...\". The\n// client-side regex patterns are used for images that are part of an\n// \"src\" attribute with a base64 encoded svg in the <img> tag. Perhaps we should\n// consider finding a solution to define them only once? The issue is that the\n// regex patterns in Python are slightly different from those in JavaScript.\n// See : controllers/main.py\nconst CSS_ANIMATION_RULE_REGEX =\n    /(?<declaration>animation(?:-duration)?: .*?)(?<value>(?:\\d+(?:\\.\\d+)?)|(?:\\.\\d+))(?<unit>ms|s)(?<separator>\\s|;|\"|$)/gm;\nconst SVG_DUR_TIMECOUNT_VAL_REGEX =\n    /(?<attribute_name>\\sdur=\"\\s*)(?<value>(?:\\d+(?:\\.\\d+)?)|(?:\\.\\d+))(?<unit>h|min|ms|s)?\\s*\"/gm;\nconst CSS_ANIMATION_RATIO_REGEX = /(--animation_ratio: (?<ratio>\\d*(\\.\\d+)?));/m;\n/**\n * Caches rpc/orm service\n * @param {Function} service\n * @param  {...any} args\n * @returns\n */\nfunction serviceCached(service) {\n    const cache = _serviceCache;\n    return Object.assign(Object.create(service), {\n        call() {\n            // FIXME\n            const serviceName = Object.prototype.hasOwnProperty.call(service, \"call\")\n                ? \"orm\"\n                : \"rpc\";\n            const cacheId = JSON.stringify(arguments);\n            if (!cache[serviceName][cacheId]) {\n                cache[serviceName][cacheId] =\n                    serviceName == \"rpc\" ? service(...arguments) : service.call(...arguments);\n            }\n            return cache[serviceName][cacheId];\n        },\n    });\n}\n// Outdated snippets whose alert has been discarded.\nconst controlledSnippets = new Set();\nconst clearControlledSnippets = () => controlledSnippets.clear();\n/**\n * @param {HTMLElement} el\n * @param {string} [title]\n * @param {Object} [options]\n * @param {string[]} [options.classes]\n * @param {string} [options.tooltip]\n * @param {string} [options.placeholder]\n * @param {Object} [options.dataAttributes]\n * @returns {HTMLElement} - the original 'el' argument\n */\nfunction _addTitleAndAllowedAttributes(el, title, options) {\n    let tooltipEl = el;\n    if (title) {\n        const titleEl = _buildTitleElement(title);\n        tooltipEl = titleEl;\n        el.appendChild(titleEl);\n        if (options && options.dataAttributes && options.dataAttributes.fontFamily) {\n            titleEl.style.fontFamily = options.dataAttributes.fontFamily;\n        }\n    }\n\n    if (options && options.classes) {\n        el.classList.add(...options.classes);\n    }\n    if (options && options.tooltip) {\n        tooltipEl.title = options.tooltip;\n    }\n    if (options && options.placeholder) {\n        el.setAttribute('placeholder', options.placeholder);\n    }\n    if (options && options.dataAttributes) {\n        for (const key in options.dataAttributes) {\n            el.dataset[key] = options.dataAttributes[key];\n        }\n    }\n\n    return el;\n}\n/**\n * @param {string} tagName\n * @param {string} title - @see _addTitleAndAllowedAttributes\n * @param {Object} options - @see _addTitleAndAllowedAttributes\n * @returns {HTMLElement}\n */\nfunction _buildElement(tagName, title, options) {\n    const el = document.createElement(tagName);\n    return _addTitleAndAllowedAttributes(el, title, options);\n}\n/**\n * @param {string} title\n * @returns {HTMLElement}\n */\nfunction _buildTitleElement(title) {\n    const titleEl = document.createElement('we-title');\n    titleEl.textContent = title;\n    return titleEl;\n}\n/**\n * @param {string} src\n * @returns {HTMLElement}\n */\nconst _buildImgElementCache = {};\nasync function _buildImgElement(src) {\n    if (!(src in _buildImgElementCache)) {\n        _buildImgElementCache[src] = (async () => {\n            let text;\n            if (src.split('.').pop() === 'svg') {\n                try {\n                    const response = await window.fetch(src);\n                    text = await response.text();\n                } catch {\n                    // In some tours, the tour finishes before the fetch is done\n                    // and when a tour is finished, the python side will ask the\n                    // browser to stop loading resources. This causes the fetch\n                    // to fail and throw an error which crashes the test even\n                    // though it completed successfully.\n                    // So return an empty SVG to ensure everything completes\n                    // correctly.\n                    text = \"<svg></svg>\";\n                }\n                const parser = new window.DOMParser();\n                const xmlDoc = parser.parseFromString(text, 'text/xml');\n                return xmlDoc.getElementsByTagName('svg')[0];\n            } else {\n                const imgEl = document.createElement('img');\n                imgEl.src = src;\n                return imgEl;\n            }\n        })();\n    }\n    const node = await _buildImgElementCache[src];\n    return node.cloneNode(true);\n}\n/**\n * Build the correct DOM for a we-row element.\n *\n * @param {string} [title] - @see _buildElement\n * @param {Object} [options] - @see _buildElement\n * @param {HTMLElement[]} [options.childNodes]\n * @returns {HTMLElement}\n */\nfunction _buildRowElement(title, options) {\n    const groupEl = _buildElement('we-row', title, options);\n\n    const rowEl = document.createElement('div');\n    groupEl.appendChild(rowEl);\n\n    if (options && options.childNodes) {\n        options.childNodes.forEach(node => rowEl.appendChild(node));\n    }\n\n    return groupEl;\n}\n/**\n * Build the correct DOM for a we-collapse element.\n *\n * @param {string} [title] - @see _buildElement\n * @param {Object} [options] - @see _buildElement\n * @param {HTMLElement[]} [options.childNodes]\n * @returns {HTMLElement}\n */\nfunction _buildCollapseElement(title, options) {\n    const groupEl = _buildElement('we-collapse', title, options);\n    const titleEl = groupEl.querySelector('we-title');\n\n    const children = options && options.childNodes || [];\n    if (titleEl) {\n        titleEl.remove();\n        titleEl.classList.add('o_we_collapse_toggler');\n        children.unshift(titleEl);\n    }\n    let i = 0;\n    for (i = 0; i < children.length; i++) {\n        groupEl.appendChild(children[i]);\n        if (children[i].nodeType === Node.ELEMENT_NODE) {\n            break;\n        }\n    }\n\n    const togglerEl = document.createElement('we-toggler');\n    togglerEl.classList.add('o_we_collapse_toggler');\n    groupEl.appendChild(togglerEl);\n\n    const containerEl = document.createElement('div');\n    children.slice(i + 1).forEach(node => containerEl.appendChild(node));\n    groupEl.appendChild(containerEl);\n\n    return groupEl;\n}\n/**\n * Creates a proxy for an object where one property is replaced by a different\n * value. This value is captured in the closure and can be read and written to.\n *\n * @param {Object} obj - the object for which to create a proxy\n * @param {string} propertyName - the name/key of the property to replace\n * @param {*} value - the initial value to give to the property's copy\n * @returns {Proxy} a proxy of the object with the property replaced\n */\nfunction createPropertyProxy(obj, propertyName, value) {\n    return new Proxy(obj, {\n        get: function (obj, prop) {\n            if (prop === propertyName) {\n                return value;\n            }\n            return obj[prop];\n        },\n        set: function (obj, prop, val) {\n            if (prop === propertyName) {\n                return (value = val);\n            }\n            return Reflect.set(...arguments);\n        },\n    });\n}\n/**\n * Creates and registers a UserValueWidget by tag-name\n *\n * @param {string} widgetName\n * @param {SnippetOptionWidget|UserValueWidget|null} parent\n * @param {string} title\n * @param {Object} options\n * @returns {UserValueWidget}\n */\nfunction registerUserValueWidget(widgetName, parent, title, options, $target) {\n    const widget = new userValueWidgetsRegistry[widgetName](parent, title, options, $target);\n    parent.registerSubWidget(widget);\n    return widget;\n}\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\nconst NULL_ID = '__NULL__';\n\n/**\n * Base class for components to be used in snippet options widgets to retrieve\n * user values.\n */\nconst UserValueWidget = publicWidget.Widget.extend({\n    className: 'o_we_user_value_widget',\n    custom_events: {\n        'user_value_update': '_onUserValueNotification',\n    },\n\n    /**\n     * @constructor\n     */\n    init: function (parent, title, options, $target) {\n        this._super(...arguments);\n        this.title = title;\n        this.options = options;\n        this._userValueWidgets = [];\n        this._value = '';\n        this.$target = $target;\n    },\n    /**\n     * @override\n     */\n    async willStart() {\n        await this._super(...arguments);\n        if (this.options.dataAttributes.img) {\n            this.illustrationEl = await _buildImgElement(this.options.dataAttributes.img);\n        } else if (this.options.dataAttributes.icon) {\n            this.illustrationEl = document.createElement('i');\n            this.illustrationEl.classList.add('fa', this.options.dataAttributes.icon);\n        }\n        if (this.options.dataAttributes.reload) {\n            this.options.dataAttributes.noPreview = \"true\";\n        }\n    },\n    /**\n     * @override\n     */\n    _makeDescriptive: function () {\n        const $el = this._super(...arguments);\n        const el = $el[0];\n        _addTitleAndAllowedAttributes(el, this.title, this.options);\n        this.containerEl = document.createElement('div');\n\n        if (this.illustrationEl) {\n            this.containerEl.appendChild(this.illustrationEl);\n        }\n\n        el.appendChild(this.containerEl);\n        return $el;\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        if (this.el.classList.contains('o_we_img_animate')) {\n            const buildImgExtensionSwitcher = (from, to) => {\n                const regex = new RegExp(`${from}$`, 'i');\n                return ev => {\n                    const img = ev.currentTarget.getElementsByTagName(\"img\")[0];\n                    img.src = img.src.replace(regex, to);\n                };\n            };\n            this.$el.on('mouseenter.img_animate', buildImgExtensionSwitcher('png', 'gif'));\n            this.$el.on('mouseleave.img_animate', buildImgExtensionSwitcher('gif', 'png'));\n        }\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        // Check if $el exists in case the widget is destroyed before it has\n        // been fully initialized.\n        // TODO there is probably better to do. This case was found only in\n        // tours, where the editor is left before the widget icon is loaded.\n        if (this.$el) {\n            this.$el.off('.img_animate');\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Closes the widget (only meaningful for widgets that can be closed).\n     */\n    close: function () {\n        if (!this.el) {\n            // In case the method is called while the widget is not fully\n            // initialized yet. No need to prevent that case: asking a non\n            // initialized widget to close itself should just not be a problem\n            // and just be ignored.\n            return;\n        }\n        if (!this.el.classList.contains('o_we_widget_opened')) {\n            // Small optimization: it would normally not matter asking to\n            // remove a class of an element if it does not already have it but\n            // in this case we do more: we trigger_up an event and ask to close\n            // all sub widgets. When we ask the editor to close all widgets...\n            // it makes sense not letting every sub button of every select\n            // trigger_up an event. This allows to avoid tens of thousands of\n            // instructions being done at each click in the editor.\n            return;\n        }\n        this.trigger_up('user_value_widget_closing');\n        this.el.classList.remove('o_we_widget_opened');\n        this._userValueWidgets.forEach(widget => widget.close());\n    },\n    /**\n     * Simulates the correct event on the element to make it active.\n     */\n    enable() {\n        this.$el.click();\n    },\n    /**\n     * @param {string} name\n     * @returns {UserValueWidget|null}\n     */\n    findWidget: function (name) {\n        for (const widget of this._userValueWidgets) {\n            if (widget.getName() === name) {\n                return widget;\n            }\n            const depWidget = widget.findWidget(name);\n            if (depWidget) {\n                return depWidget;\n            }\n        }\n        return null;\n    },\n    /**\n     * Focus the main focusable element of the widget.\n     */\n    focus() {\n        const el = this._getFocusableElement();\n        if (el) {\n            el.focus();\n        }\n    },\n    /**\n     * Returns the value that the widget would hold if it was active, by default\n     * the internal value it holds.\n     *\n     * @param {string} [methodName]\n     * @returns {string}\n     */\n    getActiveValue: function (methodName) {\n        return this._value;\n    },\n    /**\n     * Returns the default value the widget holds when inactive, by default the\n     * first \"possible value\".\n     *\n     * @param {string} [methodName]\n     * @returns {string}\n     */\n    getDefaultValue: function (methodName) {\n        const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n        return possibleValues && possibleValues[0] || '';\n    },\n    /**\n     * @returns {string[]}\n     */\n    getDependencies: function () {\n        return this._dependencies;\n    },\n    /**\n     * Returns the names of the option methods associated to the widget. Those\n     * are loaded with @see loadMethodsData.\n     *\n     * @returns {string[]}\n     */\n    getMethodsNames: function () {\n        return this._methodsNames;\n    },\n    /**\n     * Returns the option parameters associated to the widget (for a given\n     * method name or not). Most are loaded with @see loadMethodsData.\n     *\n     * @param {string} [methodName]\n     * @returns {Object}\n     */\n    getMethodsParams: function (methodName) {\n        const params = Object.assign({}, this._methodsParams);\n        if (methodName) {\n            params.possibleValues = params.optionsPossibleValues[methodName] || [];\n            params.activeValue = this.getActiveValue(methodName);\n            params.defaultValue = this.getDefaultValue(methodName);\n        }\n        return params;\n    },\n    /**\n     * @returns {string} empty string if no name is used by the widget\n     */\n    getName: function () {\n        return this._methodsParams.name || '';\n    },\n    /**\n     * Returns the user value that the widget currently holds. The value is a\n     * string, this is the value that will be received in the option methods\n     * of SnippetOptionWidget instances.\n     *\n     * @param {string} [methodName]\n     * @returns {string}\n     */\n    getValue: function (methodName) {\n        const isActive = this.isActive();\n        if (!methodName || !this._methodsNames.includes(methodName)) {\n            return isActive ? 'true' : '';\n        }\n        if (isActive) {\n            return this.getActiveValue(methodName);\n        }\n        return this.getDefaultValue(methodName);\n    },\n    /**\n     * Returns whether or not the widget is active (holds a value).\n     *\n     * @returns {boolean}\n     */\n    isActive: function () {\n        return this._value && this._value !== NULL_ID;\n    },\n    /**\n     * Indicates if the widget can contain sub user value widgets or not.\n     *\n     * @returns {boolean}\n     */\n    isContainer: function () {\n        return false;\n    },\n    /**\n     * Indicates if the widget is being previewed or not: the user is\n     * manipulating it. Base case: if an internal <input/> element is focused.\n     *\n     * @returns {boolean}\n     */\n    isPreviewed: function () {\n        const focusEl = document.activeElement;\n        if (focusEl && focusEl.tagName === 'INPUT'\n                && (this.el === focusEl || this.el.contains(focusEl))) {\n            return true;\n        }\n        return this.el.classList.contains('o_we_preview');\n    },\n    /**\n     * Loads option method names and option method parameters.\n     *\n     * @param {string[]} validMethodNames\n     * @param {Object} extraParams\n     */\n    loadMethodsData: function (validMethodNames, extraParams) {\n        this._methodsNames = [];\n        this._methodsParams = Object.assign({}, extraParams);\n        this._methodsParams.optionsPossibleValues = {};\n        this._dependencies = [];\n        this._triggerWidgetsNames = [];\n        this._triggerWidgetsValues = [];\n\n        for (const key in this.el.dataset) {\n            const dataValue = this.el.dataset[key].trim();\n\n            if (key === 'dependencies') {\n                this._dependencies.push(...dataValue.split(/\\s*,\\s*/g));\n            } else if (key === 'trigger') {\n                this._triggerWidgetsNames.push(...dataValue.split(/\\s*,\\s*/g));\n            } else if (key === 'triggerValue') {\n                this._triggerWidgetsValues.push(...dataValue.split(/\\s*,\\s*/g));\n            } else if (validMethodNames.includes(key)) {\n                this._methodsNames.push(key);\n                this._methodsParams.optionsPossibleValues[key] = dataValue.split(/\\s*\\|\\s*/g);\n            } else {\n                this._methodsParams[key] = dataValue;\n            }\n        }\n        this._userValueWidgets.forEach(widget => {\n            const inheritedParams = Object.assign({}, this._methodsParams);\n            inheritedParams.optionsPossibleValues = null;\n            widget.loadMethodsData(validMethodNames, inheritedParams);\n            const subMethodsNames = widget.getMethodsNames();\n            const subMethodsParams = widget.getMethodsParams();\n\n            for (const methodName of subMethodsNames) {\n                if (!this._methodsNames.includes(methodName)) {\n                    this._methodsNames.push(methodName);\n                    this._methodsParams.optionsPossibleValues[methodName] = [];\n                }\n                for (const subPossibleValue of subMethodsParams.optionsPossibleValues[methodName]) {\n                    this._methodsParams.optionsPossibleValues[methodName].push(subPossibleValue);\n                }\n            }\n        });\n        for (const methodName of this._methodsNames) {\n            const arr = this._methodsParams.optionsPossibleValues[methodName];\n            const uniqArr = arr.filter((v, i, arr) => i === arr.indexOf(v));\n            this._methodsParams.optionsPossibleValues[methodName] = uniqArr;\n        }\n\n        // Method names come from the widget's dataset whose keys' order cannot\n        // be relied on. We explicitely sort them by alphabetical order allowing\n        // consistent behavior, while relying on order for such methods should\n        // not be done when possible (the methods should be independent from\n        // each other when possible).\n        this._methodsNames.sort();\n    },\n    /**\n     * @param {boolean} [previewMode=false]\n     * @param {boolean} [isSimulatedEvent=false]\n     */\n    notifyValueChange: function (previewMode, isSimulatedEvent) {\n        // In the case we notify a change update, force a preview update if it\n        // was not already previewed\n        const isPreviewed = this.isPreviewed();\n        if (!previewMode && !isPreviewed) {\n            this.notifyValueChange(true);\n        }\n\n        const data = {\n            previewMode: previewMode || false,\n            isSimulatedEvent: !!isSimulatedEvent,\n        };\n        // TODO improve this. The preview state has to be updated only when the\n        // actual option _select is gonna be called... but this is delayed by a\n        // mutex. So, during test tours, we would notify both 'preview' and\n        // 'reset' before the 'preview' handling is done: and so the widget\n        // would not be considered in preview during that 'preview' handling.\n        if (previewMode === true || previewMode === false) {\n            // Note: the widgets need to be considered in preview mode during\n            // non-preview handling (a previewed checkbox is considered having\n            // an inverted state)... but if, for example, a modal opens before\n            // handling that non-preview, a 'reset' will be thrown thus removing\n            // the preview class. So we force it in non-preview too.\n            data.prepare = () => this.el.classList.add('o_we_preview');\n        } else if (previewMode === 'reset') {\n            data.prepare = () => this.el.classList.remove('o_we_preview');\n        }\n\n        this.trigger_up('user_value_update', data);\n    },\n    /**\n     * Opens the widget (only meaningful for widgets that can be opened).\n     */\n    open() {\n        this.trigger_up('user_value_widget_opening');\n        this.el.classList.add('o_we_widget_opened');\n    },\n    /**\n     * Adds the given widget to the known list of user value sub-widgets (useful\n     * for container widgets).\n     *\n     * @param {UserValueWidget} widget\n     */\n    registerSubWidget: function (widget) {\n        this._userValueWidgets.push(widget);\n    },\n    /**\n     * Sets the user value that the widget should currently hold, for the\n     * given method name.\n     *\n     * Note: a widget typically only holds one value for the only method it\n     * supports. However, widgets can have several methods; in that case, the\n     * value is typically received for a first method and receiving the value\n     * for other ones should not affect the widget (otherwise, it means the\n     * methods are conflicting with each other).\n     *\n     * @param {string} value\n     * @param {string} [methodName]\n     */\n    async setValue(value, methodName) {\n        this._value = value;\n        this.el.classList.remove('o_we_preview');\n    },\n    /**\n     * @param {boolean} show\n     */\n    toggleVisibility: function (show) {\n        let doFocus = false;\n        if (show) {\n            const wasInvisible = this.el.classList.contains('d-none');\n            doFocus = wasInvisible && this.el.dataset.requestFocus === \"true\";\n        }\n        this.el.classList.toggle('d-none', !show);\n        if (doFocus) {\n            this.focus();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns the main focusable element of the widget. By default supposes\n     * nothing is focusable.\n     *\n     * @todo review all specific widget's method\n     * @private\n     * @returns {HTMLElement}\n     */\n    _getFocusableElement: function () {\n        return null;\n    },\n    /**\n     * @private\n     * @param {OdooEvent|Event}\n     * @returns {boolean}\n     */\n    _handleNotifierEvent: function (ev) {\n        if (!ev) {\n            return true;\n        }\n        if (ev._seen) {\n            return false;\n        }\n        ev._seen = true;\n        if (ev.preventDefault) {\n            ev.preventDefault();\n        }\n        return true;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Should be called when an user event on the widget indicates a value\n     * change.\n     *\n     * @private\n     * @param {OdooEvent|Event} [ev]\n     */\n    _onUserValueChange: function (ev) {\n        if (this._handleNotifierEvent(ev)) {\n            this.notifyValueChange(false);\n        }\n    },\n    /**\n     * Allows container widgets to add additional data if needed.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onUserValueNotification: function (ev) {\n        ev.data.widget = this;\n\n        if (!ev.data.triggerWidgetsNames) {\n            ev.data.triggerWidgetsNames = [];\n        }\n        ev.data.triggerWidgetsNames.push(...this._triggerWidgetsNames);\n\n        if (!ev.data.triggerWidgetsValues) {\n            ev.data.triggerWidgetsValues = [];\n        }\n        ev.data.triggerWidgetsValues.push(...this._triggerWidgetsValues);\n    },\n    /**\n     * Should be called when an user event on the widget indicates a value\n     * preview.\n     *\n     * @private\n     * @param {OdooEvent|Event} [ev]\n     */\n    _onUserValuePreview: function (ev) {\n        if (this._handleNotifierEvent(ev)) {\n            this.notifyValueChange(true);\n        }\n    },\n    /**\n     * Should be called when an user event on the widget indicates a value\n     * reset.\n     *\n     * @private\n     * @param {OdooEvent|Event} [ev]\n     */\n    _onUserValueReset: function (ev) {\n        if (this._handleNotifierEvent(ev)) {\n            this.notifyValueChange('reset');\n        }\n    },\n});\n\nconst ButtonUserValueWidget = UserValueWidget.extend({\n    tagName: 'we-button',\n    events: {\n        'click': '_onButtonClick',\n        'click [role=\"button\"]': '_onInnerButtonClick',\n        'mouseenter': '_onUserValuePreview',\n        'mouseleave': '_onUserValueReset',\n    },\n\n    /**\n     * @override\n     */\n    async willStart() {\n        await this._super(...arguments);\n        if (this.options.dataAttributes.activeImg) {\n            this.activeImgEl = await _buildImgElement(this.options.dataAttributes.activeImg);\n        }\n    },\n    /**\n     * @override\n     */\n    _makeDescriptive() {\n        const $el = this._super(...arguments);\n        if (this.illustrationEl) {\n            $el[0].classList.add('o_we_icon_button');\n        }\n        if (this.activeImgEl) {\n            this.containerEl.appendChild(this.activeImgEl);\n        }\n        return $el;\n    },\n    /**\n     * @override\n     */\n    start: function (parent, title, options) {\n        if (this.options && this.options.childNodes) {\n            this.options.childNodes.forEach(node => this.containerEl.appendChild(node));\n        }\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getActiveValue: function (methodName) {\n        const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n        return possibleValues && possibleValues[possibleValues.length - 1] || '';\n    },\n    /**\n     * @override\n     */\n    isActive: function () {\n        return (this.isPreviewed() !== this.el.classList.contains('active'));\n    },\n    /**\n     * @override\n     */\n    loadMethodsData: function (validMethodNames) {\n        this._super.apply(this, arguments);\n        for (const methodName of this._methodsNames) {\n            const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n            if (possibleValues.length <= 1) {\n                possibleValues.unshift('');\n            }\n        }\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        await this._super(...arguments);\n        let active = !!value;\n        if (methodName) {\n            if (!this._methodsNames.includes(methodName)) {\n                return;\n            }\n            active = (this.getActiveValue(methodName) === value);\n        }\n        if (this.illustrationEl && this.activeImgEl) {\n            this.illustrationEl.classList.toggle('d-none', active);\n            this.activeImgEl.classList.toggle('d-none', !active);\n        }\n        this.el.classList.toggle('active', active);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onButtonClick: function (ev) {\n        if (!ev._innerButtonClicked) {\n            this._onUserValueChange(ev);\n        }\n    },\n    /**\n     * @private\n     */\n    _onInnerButtonClick: function (ev) {\n        // Cannot just stop propagation as the click needs to be propagated to\n        // potential parent widgets for event delegation on those inner buttons.\n        ev._innerButtonClicked = true;\n    },\n});\n\nconst CheckboxUserValueWidget = ButtonUserValueWidget.extend({\n    className: (ButtonUserValueWidget.prototype.className || '') + ' o_we_checkbox_wrapper',\n\n    /**\n     * @override\n     */\n    start: function () {\n        const checkboxEl = document.createElement('we-checkbox');\n        this.containerEl.appendChild(checkboxEl);\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    enable() {\n        this.$('we-checkbox').click();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onButtonClick(ev) {\n        if (!ev.target.closest('we-title, we-checkbox')) {\n            // Only consider clicks on the label and the checkbox control itself\n            return;\n        }\n        return this._super(...arguments);\n    },\n});\n\nconst BaseSelectionUserValueWidget = UserValueWidget.extend({\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        this.menuEl = document.createElement('we-selection-items');\n        if (this.options && this.options.childNodes) {\n            this.options.childNodes.forEach(node => {\n                // Ensure to only put element nodes inside the selection menu\n                // as there could be an :empty CSS rule to handle the case when\n                // the menu is empty (so it should not contain any whitespace).\n                if (node.nodeType === Node.ELEMENT_NODE) {\n                    this.menuEl.appendChild(node);\n                }\n            });\n        }\n        this.containerEl.appendChild(this.menuEl);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams(methodName) {\n        const params = this._super(...arguments);\n        const activeWidget = this._getActiveSubWidget();\n        if (!activeWidget) {\n            return params;\n        }\n        return Object.assign(activeWidget.getMethodsParams(...arguments), params);\n    },\n    /**\n     * @override\n     */\n    getValue(methodName) {\n        const activeWidget = this._getActiveSubWidget();\n        if (activeWidget) {\n            return activeWidget.getActiveValue(methodName);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    isContainer() {\n        return true;\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        const _super = this._super.bind(this);\n        for (const widget of this._userValueWidgets) {\n            await widget.setValue(NULL_ID, methodName);\n        }\n        for (const widget of [...this._userValueWidgets].reverse()) {\n            await widget.setValue(value, methodName);\n            if (widget.isActive()) {\n                // Only one select item can be true at a time, we consider the\n                // last one if multiple would be active.\n                break;\n            }\n        }\n        await _super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @returns {UserValueWidget|undefined}\n     */\n    _getActiveSubWidget() {\n        const previewedWidget = this._userValueWidgets.find(widget => widget.isPreviewed());\n        if (previewedWidget) {\n            return previewedWidget;\n        }\n        return this._userValueWidgets.find(widget => widget.isActive());\n    },\n});\n\nconst SelectUserValueWidget = BaseSelectionUserValueWidget.extend({\n    tagName: 'we-select',\n    events: {\n        'click': '_onClick',\n    },\n    PLACEHOLDER_TEXT: _t(\"None\"),\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        if (this.options && this.options.valueEl) {\n            this.containerEl.insertBefore(this.options.valueEl, this.menuEl);\n        }\n\n        this.menuEl.dataset.placeholderText = this.PLACEHOLDER_TEXT;\n\n        this.menuTogglerEl = document.createElement('we-toggler');\n        this.menuTogglerEl.dataset.placeholderText = this.PLACEHOLDER_TEXT;\n        this.iconEl = this.illustrationEl || null;\n        const icon = this.el.dataset.icon;\n        if (icon) {\n            this.iconEl = document.createElement('i');\n            this.iconEl.classList.add('fa', 'fa-fw', icon);\n        }\n        if (this.iconEl) {\n            this.el.classList.add('o_we_icon_select');\n            this.menuTogglerEl.appendChild(this.iconEl);\n        }\n        this.containerEl.insertBefore(this.menuTogglerEl, this.menuEl);\n\n        const dropdownCaretEl = document.createElement('span');\n        dropdownCaretEl.classList.add('o_we_dropdown_caret');\n        this.containerEl.appendChild(dropdownCaretEl);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    close: function () {\n        this._super(...arguments);\n        this.el.classList.remove(\"o_we_select_dropdown_up\");\n        if (this.menuTogglerEl) {\n            this.menuTogglerEl.classList.remove('active');\n        }\n    },\n    /**\n     * @override\n     */\n    isPreviewed: function () {\n        return this._super(...arguments) || this.menuTogglerEl.classList.contains('active');\n    },\n    /**\n     * @override\n     */\n    open() {\n        this._super(...arguments);\n        this.menuTogglerEl.classList.add('active');\n        this._adjustDropdownPosition();\n    },\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n\n        if (this.iconEl) {\n            return;\n        }\n\n        if (this.menuTogglerItemEl) {\n            this.menuTogglerItemEl.remove();\n            this.menuTogglerItemEl = null;\n        }\n\n        let textContent = '';\n        const activeWidget = this._userValueWidgets.find(widget => !widget.isPreviewed() && widget.isActive());\n        if (activeWidget) {\n            const svgTag = activeWidget.el.querySelector('svg'); // useful to avoid searching text content in svg element\n            const value = (activeWidget.el.dataset.selectLabel || (!svgTag && activeWidget.el.textContent.trim()));\n            const imgSrc = activeWidget.el.dataset.img;\n            const icon = activeWidget.el.dataset.icon;\n            if (value) {\n                textContent = value;\n            } else if (icon) {\n                this.menuTogglerItemEl = document.createElement('i');\n                this.menuTogglerItemEl.classList.add('fa', icon);\n            } else if (imgSrc) {\n                this.menuTogglerItemEl = document.createElement('img');\n                this.menuTogglerItemEl.src = imgSrc;\n            } else {\n                const fakeImgEl = activeWidget.el.querySelector('.o_we_fake_img_item');\n                if (fakeImgEl) {\n                    this.menuTogglerItemEl = fakeImgEl.cloneNode(true);\n                }\n            }\n        } else {\n            textContent = this.PLACEHOLDER_TEXT;\n        }\n\n        this.menuTogglerEl.textContent = textContent;\n        if (this.menuTogglerItemEl) {\n            this.menuTogglerEl.appendChild(this.menuTogglerItemEl);\n        }\n    },\n    /**\n     * @override\n     */\n    enable() {\n        if (!this.menuTogglerEl.classList.contains('active')) {\n            this.menuTogglerEl.click();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _shouldIgnoreClick(ev) {\n        return !!ev.target.closest('[role=\"button\"]');\n    },\n    /**\n     * Decides whether the dropdown should be positioned below or above the\n     * selector based on the available space.\n     *\n     * @private\n     */\n    _adjustDropdownPosition() {\n        const customizePanelEl = this.menuEl.closest(\".o_we_customize_panel\");\n        if (!customizePanelEl) {\n            return;\n        }\n\n        this.el.classList.remove(\"o_we_select_dropdown_up\");\n        const customizePanelElCoords = customizePanelEl.getBoundingClientRect();\n        let dropdownMenuElCoords = this.menuEl.getBoundingClientRect();\n\n        // Adds a margin to prevent the dropdown from sticking to the edge of\n        // the customize panel.\n        const dropdownMenuMargin = 5;\n        // If after opening, the dropdown list overflows the customization\n        // panel at the bottom, opens the dropdown above the selector.\n        if ((dropdownMenuElCoords.bottom + dropdownMenuMargin) > customizePanelElCoords.bottom) {\n            this.el.classList.add(\"o_we_select_dropdown_up\");\n            dropdownMenuElCoords = this.menuEl.getBoundingClientRect();\n            // If there is no available space above it either, then we open\n            // it below the selector.\n            if (dropdownMenuElCoords.top < customizePanelElCoords.top) {\n                this.el.classList.remove(\"o_we_select_dropdown_up\");\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the select is clicked anywhere -> open/close it.\n     *\n     * @private\n     */\n    _onClick: function (ev) {\n        if (this._shouldIgnoreClick(ev)) {\n            return;\n        }\n\n        if (!this.menuTogglerEl.classList.contains('active')) {\n            this.open();\n        } else {\n            this.close();\n        }\n        const activeButton = this._userValueWidgets.find(widget => widget.isActive());\n        if (activeButton) {\n            this.menuEl.scrollTop = activeButton.el.offsetTop - (this.menuEl.offsetHeight / 2);\n        }\n    },\n});\n\nconst ButtonGroupUserValueWidget = BaseSelectionUserValueWidget.extend({\n    tagName: 'we-button-group',\n});\n\nconst UnitUserValueWidget = UserValueWidget.extend({\n    /**\n     * @override\n     */\n    start: async function () {\n        const unit = this.el.dataset.unit || '';\n        this.el.dataset.unit = unit;\n        if (this.el.dataset.saveUnit === undefined) {\n            this.el.dataset.saveUnit = unit;\n        }\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getActiveValue: function (methodName) {\n        const activeValue = this._super(...arguments);\n\n        const params = this._methodsParams;\n        if (!this._isNumeric()) {\n            return activeValue;\n        }\n\n        const defaultValue = this.getDefaultValue(methodName, false);\n\n        return activeValue.split(/\\s+/g).map(v => {\n            const numValue = parseFloat(v);\n            if (isNaN(numValue)) {\n                return defaultValue;\n            } else {\n                const value = weUtils.convertNumericToUnit(numValue, params.unit, params.saveUnit, params.cssProperty, this.$target);\n                return `${this._floatToStr(value)}${params.saveUnit}`;\n            }\n        }).join(' ');\n    },\n    /**\n     * @override\n     * @param {boolean} [useInputUnit=false]\n     */\n    getDefaultValue: function (methodName, useInputUnit) {\n        const defaultValue = this._super(...arguments);\n\n        const params = this._methodsParams;\n        if (!this._isNumeric()) {\n            return defaultValue;\n        }\n\n        const unit = useInputUnit ? params.unit : params.saveUnit;\n        const numValue = weUtils.convertValueToUnit(defaultValue || '0', unit, params.cssProperty, this.$target);\n        if (isNaN(numValue)) {\n            return defaultValue;\n        }\n        return `${this._floatToStr(numValue)}${unit}`;\n    },\n    /**\n     * @override\n     */\n    isActive: function () {\n        const isSuperActive = this._super(...arguments);\n        if (!this._isNumeric()) {\n            return isSuperActive;\n        }\n        return isSuperActive && (\n            this._floatToStr(parseFloat(this._value)) !== '0'\n            // Or is a composite value.\n            || !!this._value.match(/\\d+\\s+\\d+/)\n        );\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        const params = this._methodsParams;\n        if (this._isNumeric()) {\n            value = value.split(' ').map(v => {\n                const numValue = weUtils.convertValueToUnit(v, params.unit, params.cssProperty, this.$target);\n                if (isNaN(numValue)) {\n                    return ''; // Something not supported\n                }\n                return this._floatToStr(numValue);\n            }).join(' ');\n        }\n        return this._super(value, methodName);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Converts a floating value to a string, rounded to 5 digits without zeros.\n     *\n     * @private\n     * @param {number} value\n     * @returns {string}\n     */\n    _floatToStr: function (value) {\n        return `${parseFloat(value.toFixed(5))}`;\n    },\n    /**\n     * Checks whether the widget contains a numeric value.\n     *\n     * @private\n     * @returns {Boolean} true if the value is numeric, false otherwise.\n     */\n    _isNumeric() {\n        const params = this._methodsParams || this.el.dataset;\n        return !!params.unit;\n    },\n});\n\nconst InputUserValueWidget = UnitUserValueWidget.extend({\n    tagName: 'we-input',\n    events: {\n        'input input': '_onInputInput',\n        'blur input': '_onInputBlur',\n        'change input': '_onUserValueChange',\n        'keydown input': '_onInputKeydown',\n    },\n\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n\n        const unit = this.el.dataset.unit;\n        this.inputEl = document.createElement('input');\n        this.inputEl.setAttribute('type', 'text');\n        this.inputEl.setAttribute('autocomplete', 'chrome-off');\n        this.inputEl.setAttribute('placeholder', this.el.getAttribute('placeholder') || '');\n        const useNumberAlignment = this._isNumeric() || !!this.el.dataset.hideUnit;\n        this.inputEl.classList.toggle('text-start', !useNumberAlignment);\n        this.inputEl.classList.toggle('text-end', useNumberAlignment);\n        this.containerEl.appendChild(this.inputEl);\n\n        const showUnit = (!!unit || !!this.el.dataset.fakeUnit) && !this.el.dataset.hideUnit;\n        if (showUnit) {\n            var unitEl = document.createElement('span');\n            const unitText = this.el.dataset.fakeUnit || unit;\n            unitEl.textContent = unitText;\n            this.containerEl.appendChild(unitEl);\n            if (unitText.length > 3) {\n                this.el.classList.add('o_we_large');\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n        this.inputEl.value = this._value;\n        this._oldValue = this._value;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getFocusableElement() {\n        return this.inputEl;\n    },\n    /**\n     * @override\n     */\n    _isNumeric() {\n        const isNumeric = this._super(...arguments);\n        const params = this._methodsParams || this.el.dataset;\n        return isNumeric || !!params.fakeUnit || !!params.step;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInputInput: function (ev) {\n        // First record the input value as the new current value and bound it if\n        // necessary (min / max params).\n        this._value = this.inputEl.value;\n\n        const params = this._methodsParams;\n        const hasMin = ('min' in params);\n        const hasMax = ('max' in params);\n        if (hasMin || hasMax) {\n            // Bounding the value in [min, max] if specified.\n            const boundedValue = this._value.split(/\\s+/g).map(v => {\n                let numValue = parseFloat(v);\n                if (isNaN(numValue)) {\n                    return hasMin ? params.min : v;\n                } else {\n                    numValue = hasMin ? Math.max(params.min, numValue) : numValue;\n                    numValue = hasMax ? Math.min(numValue, params.max) : numValue;\n                    return numValue;\n                }\n            }).join(\" \");\n\n            // If the bounded version is different from the value, forget about\n            // the old value so that we properly update the UI in any case.\n            this._oldValue = undefined;\n\n            // Note: we do not change the input's value because we want the user\n            // to be able to enter anything without it being auto-fixed. For\n            // example, just emptying the input to enter new numbers: you don't\n            // want the min value to pop up unexpectedly. The next UI update\n            // will take care of showing the user that the value was bound.\n            this._value = boundedValue;\n        }\n\n        // When the value changes as a result of a arrow up/down, the change\n        // event is not called, unless a real user input has been triggered.\n        // This event handler holds a variable for this in order to not call\n        // `_onUserValueChange` two times. If the users only uses arrow up/down\n        // it will be trigger on blur otherwise it will be triggered on change.\n        if (!ev.detail || !ev.detail.keyUpOrDown) {\n            this.changeEventWillBeTriggered = true;\n        }\n        this._onUserValuePreview(ev);\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInputBlur: function (ev) {\n        if (this.notifyValueChangeOnBlur && !this.changeEventWillBeTriggered) {\n            // In case the input value has been modified with arrow up/down, the\n            // change event is not triggered (except if there has been a natural\n            // input event), so if the element doesn't trigger a preview, we\n            // have to notify that the value changes now.\n            this._onUserValueChange(ev);\n            this.notifyValueChangeOnBlur = false;\n        }\n        this.changeEventWillBeTriggered = false;\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInputKeydown: function (ev) {\n        const params = this._methodsParams;\n        if (!this._isNumeric()) {\n            return;\n        }\n        switch (ev.key) {\n            case \"Enter\":\n                this._onUserValueChange(ev);\n                break;\n            case \"ArrowUp\":\n            case \"ArrowDown\": {\n                const input = ev.currentTarget;\n                let parts = (input.value || input.placeholder).match(/-?\\d+\\.\\d+|-?\\d+/g);\n                if (!parts) {\n                    parts = [input.value || input.placeholder];\n                }\n                if (parts.length > 1 && !('min' in params)) {\n                    // No negative for composite values.\n                    params['min'] = 0;\n                }\n                const newValue = parts.map(part => {\n                    let value = parseFloat(part);\n                    if (isNaN(value)) {\n                        value = 0.0;\n                    }\n                    let step = parseFloat(params.step);\n                    if (isNaN(step)) {\n                        step = 1.0;\n                    }\n\n                    const increasing = ev.key === \"ArrowUp\";\n                    const hasMin = ('min' in params);\n                    const hasMax = ('max' in params);\n\n                    // If value already at min and trying to decrease, do nothing\n                    if (!increasing && hasMin && Math.abs(value - params.min) < 0.001) {\n                        return value;\n                    }\n                    // If value already at max and trying to increase, do nothing\n                    if (increasing && hasMax && Math.abs(value - params.max) < 0.001) {\n                        return value;\n                    }\n\n                    // If trying to decrease/increase near min/max, we still need to\n                    // bound the produced value and immediately show the user.\n                    value += (increasing ? step : -step);\n                    value = hasMin ? Math.max(params.min, value) : value;\n                    value = hasMax ? Math.min(value, params.max) : value;\n                    return this._floatToStr(value);\n                }).join(\" \");\n                if (newValue === (input.value || input.placeholder)) {\n                    return;\n                }\n                input.value = newValue;\n\n                // We need to know if the change event will be triggered or not.\n                // Change is triggered if there has been a \"natural\" input event\n                // from the user. Since we are triggering a \"fake\" input event,\n                // we specify that the original event is a key up/down.\n                input.dispatchEvent(new CustomEvent('input', {\n                    bubbles: true,\n                    cancelable: true,\n                    detail: {keyUpOrDown: true}\n                }));\n                this.notifyValueChangeOnBlur = true;\n                break;\n            }\n        }\n    },\n    /**\n     * @override\n     */\n    _onUserValueChange() {\n        if (this._oldValue !== this._value) {\n            this._super(...arguments);\n        }\n    }\n});\n\nconst MultiUserValueWidget = UserValueWidget.extend({\n    tagName: 'we-multi',\n\n    /**\n     * @override\n     */\n    start: function () {\n        if (this.options && this.options.childNodes) {\n            this.options.childNodes.forEach(node => this.containerEl.appendChild(node));\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getValue: function (methodName) {\n        const value = this._userValueWidgets.map(widget => {\n            return widget.getValue(methodName);\n        }).join(' ').trim();\n\n        return value || this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    isContainer: function () {\n        return true;\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        let values = value.split(/\\s*\\|\\s*/g);\n        if (values.length === 1) {\n            values = value.split(/\\s+/g);\n        }\n        for (let i = 0; i < this._userValueWidgets.length - 1; i++) {\n            await this._userValueWidgets[i].setValue(values.shift() || '', methodName);\n        }\n        await this._userValueWidgets[this._userValueWidgets.length - 1].setValue(values.join(' '), methodName);\n    },\n});\n\nconst ColorpickerUserValueWidget = SelectUserValueWidget.extend({\n    className: (SelectUserValueWidget.prototype.className || '') + ' o_we_so_color_palette',\n\n    /**\n     * @override\n     */\n    start: async function () {\n        const _super = this._super.bind(this);\n        const args = arguments;\n\n        this.resetTabCount = 0;\n\n        // Build the select element with a custom span to hold the color preview\n        this.colorPreviewEl = document.createElement('span');\n        this.colorPreviewEl.classList.add('o_we_color_preview');\n        // todo: This div should be removed whenever possible (like when\n        // converting the uservaluewidget to owl).\n        this.colorPaletteEl = document.createElement('div');\n        this.colorPaletteEl.classList.add('o_we_color_palette_wrapper');\n        this.colorPaletteEl.style.display = 'contents';\n        this.colorPaletteColorNames = [];\n        this.options.childNodes = [this.colorPaletteEl];\n        this.options.valueEl = this.colorPreviewEl;\n        // TODO: find a better way to do this.\n        // The colorpicker widget is started before the ColorPalette component\n        // is attached to the DOM (which only happens once the user opens the\n        // picker). However, the colorNames are only set after the ColorPalette\n        // has been mounted. Initializing the colorNames through a direct call\n        // to the `getColorPickerTemplateService` so that the widget starts\n        // with possible default values is thus necessary to avoid bugs on\n        // `_computeWidgetState()`.\n        const wysiwyg = this.getParent().options.wysiwyg;\n        if (wysiwyg) {\n            const colorpickerTemplate = await wysiwyg.getColorpickerTemplate.call(wysiwyg);\n            this.colorPaletteColorNames = this._getColorNames(colorpickerTemplate);\n        }\n        return _super(...args);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    open: function () {\n        if (this.colorPaletteWrapper) {\n            this.colorPaletteWrapper?.update({\n                selectedCC: this._ccValue,\n                selectedColor: this._value,\n                resetTabCount: ++this.resetTabCount,\n            });\n            this._super(...arguments);\n        } else {\n            // TODO review in master, this does async stuff. Maybe the open\n            // method should now be async. This is not really robust as the\n            // colorPalette can be used without it to be fully rendered but\n            // the use of the saved promise where we can should mitigate that\n            // issue.\n            this._colorPaletteRenderPromise = this._renderColorPalette();\n            this._super(...arguments);\n            this._colorPaletteRenderPromise.then(() => {\n                // Re-adjust the position of the colorpicker once the\n                // colorpalette is completely rendered (once that the\n                // colorpicker has its final height.\n                // TODO should not be needed once everything will be converted\n                // to owl.\n                this._adjustDropdownPosition();\n            });\n        }\n    },\n    /**\n     * @override\n     */\n    close: function () {\n        this._super(...arguments);\n        if (this._customColorValue && this._customColorValue !== this._value) {\n            this._value = this._customColorValue;\n            this._customColorValue = false;\n            this._onUserValueChange();\n        }\n    },\n    /**\n     * @override\n     */\n    getMethodsParams: function () {\n        return Object.assign(this._super(...arguments), {\n            colorNames: this.colorPaletteColorNames,\n        });\n    },\n    /**\n     * @override\n     */\n    getValue: function (methodName) {\n        const isCCMethod = (this._methodsParams.withCombinations === methodName);\n        let value = this._super(...arguments);\n        if (isCCMethod) {\n            value = this._ccValue;\n        } else if (typeof this._customColorValue === 'string') {\n            value = this._customColorValue;\n        }\n\n        // TODO strange there is some processing below for the normal value but\n        // not for the preview value? To check in older stable versions as well.\n        if (typeof this._previewColor === 'string') {\n            return isCCMethod ? this._previewCC : this._previewColor;\n        }\n\n        if (value) {\n            // TODO probably something to be done to handle gradients properly\n            // in this code.\n            const useCssColor = this.options.dataAttributes.hasOwnProperty('useCssColor');\n            const cssCompatible = this.options.dataAttributes.hasOwnProperty('cssCompatible');\n            if ((useCssColor || cssCompatible) && !isCSSColor(value)) {\n                if (useCssColor) {\n                    value = weUtils.getCSSVariableValue(value);\n                } else {\n                    value = `var(--${value})`;\n                }\n            }\n        }\n        return value;\n    },\n    /**\n     * @override\n     */\n    isContainer: function () {\n        return false;\n    },\n    /**\n     * @override\n     */\n    isActive: function () {\n        return !!this._ccValue\n            || !weUtils.areCssValuesEqual(this._value, 'rgba(0, 0, 0, 0)');\n    },\n    /**\n     * Updates the color preview + re-render the whole color palette widget.\n     *\n     * @override\n     */\n    async setValue(color, methodName, ...rest) {\n        // The colorpicker widget can hold two values: a color combination and\n        // a normal color or a gradient. The base `_value` will hold the normal\n        // color or the gradient value. The color combination one will be\n        // available in `_ccValue`.\n        const isCCMethod = (this._methodsParams.withCombinations === methodName);\n        // Always call _super but don't change _value if meant for the CC value.\n        await this._super(isCCMethod ? this._value : color, methodName, ...rest);\n        if (isCCMethod) {\n            this._ccValue = color;\n        }\n\n        await this._colorPaletteRenderPromise;\n\n        const classes = weUtils.computeColorClasses(this.colorPaletteColorNames);\n        this.colorPreviewEl.classList.remove(...classes);\n        this.colorPreviewEl.style.removeProperty('background-color');\n        this.colorPreviewEl.style.removeProperty('background-image');\n        const prefix = this.options.dataAttributes.colorPrefix || 'bg';\n        if (this._ccValue) {\n            this.colorPreviewEl.style.backgroundColor = `var(--we-cp-o-cc${this._ccValue}-${prefix.replace(/-/, '')})`;\n            this.colorPreviewEl.style.backgroundImage = `var(--we-cp-o-cc${this._ccValue}-${prefix.replace(/-/, '')}-gradient)`;\n        }\n        if (this._value) {\n            this.colorPreviewEl.style.backgroundImage = 'none';\n            if (isCSSColor(this._value)) {\n                this.colorPreviewEl.style.backgroundColor = this._value;\n            } else if (weUtils.isColorGradient(this._value)) {\n                this.colorPreviewEl.style.backgroundImage = this._value;\n            } else if (weUtils.EDITOR_COLOR_CSS_VARIABLES.includes(this._value)) {\n                this.colorPreviewEl.style.backgroundColor = `var(--we-cp-${this._value}`;\n            } else {\n                // Checking if the className actually exists seems overkill but\n                // it is actually needed to prevent a crash. As an example, if a\n                // colorpicker widget is linked to a SnippetOption instance's\n                // `selectStyle` method designed to handle the \"border-color\"\n                // property of an element, the value received can be split if\n                // the item uses different colors for its top/right/bottom/left\n                // borders. For instance, you could receive \"red blue\" if the\n                // item as red top and bottom borders and blue left and right\n                // borders, in which case you would reach this `else` and try to\n                // add the class \"bg-red blue\" which would crash because of the\n                // space inside). In that case, we simply do not show any color.\n                // We could choose to handle this split-value case specifically\n                // but it was decided that this is enough for the moment.\n                const className = `bg-${this._value}`;\n                if (classes.includes(className)) {\n                    this.colorPreviewEl.classList.add(className);\n                }\n            }\n        }\n        // If the palette was already opened (e.g. modifying a gradient), the new DOM state must be\n        // reflected in the palette, but the tab selection must not be impacted.\n        this.colorPaletteWrapper?.update({\n            selectedCC: this._ccValue,\n            selectedColor: this._value,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _renderColorPalette: async function () {\n        this.resetTabCount = 0;\n        const options = {\n            resetTabCount: this.resetTabCount,\n            selectedCC: this._ccValue,\n            selectedColor: this._value,\n            getCustomColors: () => {\n                let result = [];\n                this.trigger_up('get_custom_colors', {\n                    onSuccess: (colors) => result = colors,\n                });\n                return result;\n            },\n            onCustomColorPicked: this._onCustomColorPicked.bind(this),\n            onColorPicked: this._onColorPicked.bind(this),\n            onColorHover: this._onColorHovered.bind(this),\n            onColorLeave: this._onColorLeft.bind(this),\n            onInputEnter: this._onEnterKey.bind(this),\n        };\n        if (this.options.dataAttributes.excluded) {\n            options.excluded = this.options.dataAttributes.excluded.replace(/ /g, '').split(',');\n        }\n        if (this.options.dataAttributes.opacity) {\n            options.opacity = parseFloat(this.options.dataAttributes.opacity);\n        }\n        if (this.options.dataAttributes.withCombinations) {\n            options.withCombinations = !!this.options.dataAttributes.withCombinations;\n        }\n        if (this.options.dataAttributes.withGradients) {\n            options.withGradients = !!this.options.dataAttributes.withGradients;\n        }\n        if (this.options.dataAttributes.noTransparency) {\n            options.noTransparency = !!this.options.dataAttributes.noTransparency;\n            options.excluded = [...(options.excluded || []), 'transparent_grayscale'];\n        }\n        if (this.options.dataAttributes.selectedTab) {\n            options.selectedTab = this.options.dataAttributes.selectedTab;\n        }\n        const wysiwyg = this.getParent().options.wysiwyg;\n        if (wysiwyg) {\n            options.document = this.$target[0].ownerDocument;\n            options.getTemplate = wysiwyg.getColorpickerTemplate.bind(wysiwyg);\n        }\n        this.colorPaletteWrapper?.destroy();\n        const sidebarDocument = this.colorPaletteEl.ownerDocument;\n        if (!(this.colorPaletteEl instanceof sidebarDocument.defaultView.HTMLElement)) {\n            // When inside an iframe, the element for mounting a component must\n            // be an instance of the iframe's HTMLElement, or else target\n            // validation for attachComponent fails.\n            const newEl = sidebarDocument.importNode(this.colorPaletteEl, true);\n            this.colorPaletteEl.before(newEl);\n            this.colorPaletteEl.remove();\n            this.colorPaletteEl = newEl;\n        }\n        this.colorPaletteWrapper = await attachComponent(this, this.colorPaletteEl, ColorPalette, options);\n    },\n    /**\n     * @override\n     */\n    _shouldIgnoreClick(ev) {\n        return ev.originalEvent.__isColorpickerClick || this._super(...arguments);\n    },\n    /**\n     * Browses the colorpicker XML template to return all possible values of\n     * [data-color].\n     *\n     * @param {string} colorpickerTemplate\n     * @returns {string[]}\n     */\n    _getColorNames(colorpickerTemplate) {\n        // Init with the color combinations presets as these don't appear in\n        // the template.\n        const colorNames = [\"1\", \"2\", \"3\", \"4\", \"5\"];\n        const template = new DOMParser().parseFromString(colorpickerTemplate, \"text/html\");\n        template.querySelectorAll(\"button[data-color]:not(.o_custom_gradient_btn)\").forEach(el => {\n            const colorName = el.dataset.color;\n            if (!weUtils.isColorGradient(colorName)) {\n                colorNames.push(colorName);\n            }\n        });\n        return colorNames;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a custom color is selected -> preview the color\n     * and set the current value. Update of this value on close\n     *\n     * @private\n     * @param {Object} params\n     */\n    _onCustomColorPicked: function (params) {\n        this._customColorValue = params.color;\n    },\n    /**\n     * Called when a color button is clicked -> confirms the preview.\n     *\n     * @private\n     * @param {Object} params\n     */\n    _onColorPicked: function (params) {\n        this._previewCC = false;\n        this._previewColor = false;\n        this._customColorValue = false;\n\n        this._ccValue = params.ccValue;\n        this._value = params.color;\n\n        this._onUserValueChange();\n    },\n    /**\n     * Called when a color button is entered -> previews the background color.\n     *\n     * @private\n     * @param {Object} params\n     */\n    _onColorHovered: function (params) {\n        this._previewCC = params.ccValue;\n        this._previewColor = params.color;\n        this._onUserValuePreview();\n    },\n    /**\n     * Called when a color button is left -> cancels the preview.\n     *\n     * @private\n     */\n    _onColorLeft: function () {\n        this._previewCC = false;\n        this._previewColor = false;\n        this._onUserValueReset();\n    },\n    /**\n     * @private\n     */\n    _onEnterKey: function () {\n        this.close();\n    },\n});\n\nconst MediapickerUserValueWidget = UserValueWidget.extend({\n    tagName: 'we-button',\n    events: {\n        'click': '_onEditMedia',\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        if (this.options.dataAttributes.buttonStyle) {\n            const iconEl = document.createElement('i');\n            iconEl.classList.add('fa', 'fa-fw', 'fa-camera');\n            $(this.containerEl).prepend(iconEl);\n        } else {\n            this.el.classList.add('o_we_no_toggle', 'o_we_bg_success');\n            this.containerEl.textContent = _t(\"Replace\");\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Creates and opens a media dialog to edit a given element's media.\n     *\n     * @private\n     * @param {HTMLElement} el the element whose media should be edited\n     * @param {boolean} [images] whether images should be available\n     *   default: false\n     * @param {boolean} [videos] whether videos should be available\n     *   default: false\n     */\n    _openDialog(el, {images = false, videos = false, save}) {\n        el.src = this._value;\n        const $editable = this.$target.closest('.o_editable');\n        this.call(\"dialog\", \"add\", MediaDialog, {\n            noImages: !images,\n            noVideos: !videos,\n            noIcons: true,\n            noDocuments: true,\n            isForBgVideo: true,\n            vimeoPreviewIds: ['528686125', '430330731', '509869821', '397142251', '763851966', '486931161',\n                '499761556', '392935303', '728584384', '865314310', '511727912', '466830211'],\n            'res_model': $editable.data('oe-model'),\n            'res_id': $editable.data('oe-id'),\n            save,\n            media: el,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n        this.el.classList.toggle('active', this.isActive());\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the edit button is clicked.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onEditMedia: function (ev) {},\n});\n\nconst ImagepickerUserValueWidget = MediapickerUserValueWidget.extend({\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onEditMedia(ev) {\n        // Need a dummy element for the media dialog to modify.\n        const dummyEl = document.createElement('img');\n        this._openDialog(dummyEl, {\n            images: true,\n            save: (media) => {\n                // Accessing the value directly through dummyEl.src converts the url to absolute,\n                // using getAttribute allows us to keep the url as it was inserted in the DOM\n                // which can be useful to compare it to values stored in db.\n                this._value = media.getAttribute('src');\n                this._onUserValueChange();\n            }\n        });\n    },\n});\n\nconst VideopickerUserValueWidget = MediapickerUserValueWidget.extend({\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onEditMedia(ev) {\n        // Need a dummy element for the media dialog to modify.\n        const dummyEl = document.createElement('iframe');\n        this._openDialog(dummyEl, {\n            videos: true,\n            save: (media) => {\n                this._value = media.querySelector('iframe').src;\n                this._onUserValueChange();\n        }});\n    },\n});\n\nconst DatetimePickerUserValueWidget = InputUserValueWidget.extend({\n    events: { // Explicitely not consider all InputUserValueWidget events\n        'blur input': '_onInputBlur',\n        'input input': '_onDateInputInput',\n    },\n    pickerType: 'datetime',\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super(...arguments);\n        this._value = DateTime.now().toUnixInteger().toString();\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n\n        this.el.classList.add('o_we_large');\n        this.inputEl.classList.add('datetimepicker-input', 'mx-0', 'text-start');\n\n        this.picker = this.call(\"datetime_picker\", \"create\", {\n            target: this.inputEl,\n            onChange: this._onDateTimePickerChange.bind(this),\n            pickerProps: {\n                type: this.pickerType,\n                minDate: DateTime.fromObject({ year: 1000 }),\n                maxDate: DateTime.now().plus({ year: 200 }),\n                value: DateTime.fromSeconds(parseInt(this._value)),\n                rounding: 0,\n            },\n        });\n        this.picker.enable();\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams: function () {\n        return Object.assign(this._super(...arguments), {\n            format: this.defaultFormat,\n        });\n    },\n    /**\n     * @override\n     */\n    isPreviewed: function () {\n        return this._super(...arguments) || this.picker.isOpen;\n    },\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n        let dateTime = null;\n        if (this._value) {\n            dateTime = DateTime.fromSeconds(parseInt(this._value))\n            if (!dateTime.isValid) {\n                dateTime = DateTime.now();\n            }\n        }\n        this.picker.state.value = dateTime;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onDateTimePickerChange: function (newDateTime) {\n        if (!newDateTime || !newDateTime.isValid) {\n            this._value = '';\n        } else {\n            this._value = newDateTime.toUnixInteger().toString();\n        }\n        this._onUserValuePreview();\n    },\n    /**\n     * Handles the clear button of the datepicker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onDateInputInput(ev) {\n        if (!this.inputEl.value) {\n            this._value = '';\n            this._onUserValuePreview(ev);\n        }\n    },\n});\n\nconst DatePickerUserValueWidget = DatetimePickerUserValueWidget.extend({\n    pickerType: 'date',\n});\n\nconst ListUserValueWidget = UserValueWidget.extend({\n    tagName: 'we-list',\n    events: {\n        'click we-button.o_we_select_remove_option': '_onRemoveItemClick',\n        'click we-button.o_we_list_add_optional': '_onAddCustomItemClick',\n        'click we-button.o_we_list_add_existing': '_onAddExistingItemClick',\n        'click we-select.o_we_user_value_widget.o_we_add_list_item': '_onAddItemSelectClick',\n        'click we-button.o_we_checkbox_wrapper': '_onAddItemCheckboxClick',\n        'input table input': '_onListItemBlurInput',\n        'blur table input': '_onListItemBlurInput',\n        'mousedown': '_onWeListMousedown',\n    },\n\n    /**\n     * @override\n     */\n    willStart() {\n        if (this.options.createWidget) {\n            this.createWidget = this.options.createWidget;\n            this.createWidget.setParent(this);\n            this.registerSubWidget(this.createWidget);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start() {\n        this.addItemTitle = this.el.dataset.addItemTitle || _t(\"Add\");\n        if (this.el.dataset.availableRecords) {\n            this.records = JSON.parse(this.el.dataset.availableRecords);\n        } else {\n            this.isCustom = !this.el.dataset.notEditable;\n        }\n        if (this.el.dataset.defaults || this.el.dataset.hasDefault) {\n            this.hasDefault = this.el.dataset.hasDefault || 'unique';\n            this.selected = this.el.dataset.defaults ? JSON.parse(this.el.dataset.defaults) : [];\n        }\n        this.listTable = document.createElement('table');\n        const tableWrapper = document.createElement('div');\n        tableWrapper.classList.add('o_we_table_wrapper');\n        tableWrapper.appendChild(this.listTable);\n        this.containerEl.appendChild(tableWrapper);\n        this.el.classList.add('o_we_fw');\n        this._makeListItemsSortable();\n        if (this.createWidget) {\n            return this.createWidget.appendTo(this.containerEl);\n        }\n    },\n\n    /**\n     * @override\n     */\n    destroy() {\n        this.bindedSortable?.cleanup();\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams() {\n        return Object.assign(this._super(...arguments), {\n            records: this.records,\n        });\n    },\n    /**\n     * @override\n     */\n    setValue() {\n        this._super(...arguments);\n        const currentValues = this._value ? JSON.parse(this._value) : [];\n        this.listTable.innerHTML = '';\n        if (this.addItemButton) {\n            this.addItemButton.remove();\n        }\n\n        if (this.createWidget) {\n            const selectedIds = currentValues.map(({ id }) => id)\n                .filter(id => typeof id === 'number');\n            // Note: it's important to simplify the domain at its maximum as the\n            // rpc using it are cached. Similar domain components should be\n            // written the same way for the cache to work.\n            this.createWidget.options.domainComponents.selected = selectedIds.length ? ['id', 'not in', selectedIds] : null;\n            this.createWidget.setValue('');\n            this.createWidget.inputEl.value = '';\n            $(this.createWidget.inputEl).trigger('input');\n        } else {\n            if (this.isCustom) {\n                this.addItemButton = document.createElement('we-button');\n                this.addItemButton.textContent = this.addItemTitle;\n                this.addItemButton.classList.add('o_we_list_add_optional');\n            } else {\n                // TODO use a real select widget ?\n                this.addItemButton = document.createElement('we-select');\n                this.addItemButton.classList.add('o_we_user_value_widget', 'o_we_add_list_item');\n                const divEl = document.createElement('div');\n                this.addItemButton.appendChild(divEl);\n                const togglerEl = document.createElement('we-toggler');\n                togglerEl.textContent = this.addItemTitle;\n                divEl.appendChild(togglerEl);\n                this.selectMenuEl = document.createElement('we-selection-items');\n                divEl.appendChild(this.selectMenuEl);\n            }\n            this.containerEl.appendChild(this.addItemButton);\n        }\n        currentValues.forEach(value => {\n            if (typeof value === 'object') {\n                const recordData = value;\n                const { id, display_name } = recordData;\n                delete recordData.id;\n                delete recordData.display_name;\n                this._addItemToTable(id, display_name, recordData);\n            } else {\n                this._addItemToTable(value, value);\n            }\n        });\n        if (!this.createWidget && !this.isCustom) {\n            this._reloadSelectDropdown(currentValues);\n        }\n        this._makeListItemsSortable();\n    },\n    /**\n     * @override\n     */\n    getValue(methodName) {\n        if (this.createWidget && this.createWidget.getMethodsNames().includes(methodName)) {\n            return this.createWidget.getValue(methodName);\n        }\n        return this._value;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {string || integer} id\n     * @param {string} [value]\n     * @param {Object} [recordData] key, values that will be added to the\n     *     element's dataset\n     */\n    _addItemToTable(id, value = this.el.dataset.defaultValue || _t(\"Item\"), recordData) {\n        const trEl = document.createElement('tr');\n        if (!this.el.dataset.unsortable) {\n            const draggableEl = document.createElement('we-button');\n            draggableEl.classList.add('o_we_drag_handle', 'o_we_link', 'fa', 'fa-fw', 'fa-arrows');\n            draggableEl.dataset.noPreview = 'true';\n            const draggableTdEl = document.createElement('td');\n            draggableTdEl.appendChild(draggableEl);\n            trEl.appendChild(draggableTdEl);\n        }\n        let recordDataSelected = false;\n        const inputEl = document.createElement('input');\n        inputEl.type = this.el.dataset.inputType || 'text';\n        if (value) {\n            inputEl.value = value;\n        }\n        if (id) {\n            inputEl.name = id;\n        }\n        if (recordData) {\n            recordDataSelected = recordData.selected;\n            if (recordData.placeholder) {\n                inputEl.placeholder = recordData.placeholder;\n            }\n            for (const key of Object.keys(recordData)) {\n                inputEl.dataset[key] = recordData[key];\n            }\n        }\n        inputEl.disabled = !this.isCustom;\n        const inputTdEl = document.createElement('td');\n        inputTdEl.classList.add('o_we_list_record_name');\n        inputTdEl.appendChild(inputEl);\n        trEl.appendChild(inputTdEl);\n        if (this.hasDefault) {\n            const checkboxEl = document.createElement('we-button');\n            checkboxEl.classList.add('o_we_user_value_widget', 'o_we_checkbox_wrapper');\n            if (this.selected.includes(id) || recordDataSelected) {\n                checkboxEl.classList.add('active');\n            }\n            if (!recordData || !recordData.notToggleable) {\n                const div = document.createElement('div');\n                const checkbox = document.createElement('we-checkbox');\n                div.appendChild(checkbox);\n                checkboxEl.appendChild(div);\n                checkboxEl.appendChild(checkbox);\n                const checkboxTdEl = document.createElement('td');\n                checkboxTdEl.appendChild(checkboxEl);\n                trEl.appendChild(checkboxTdEl);\n            }\n        }\n        if (!recordData || !recordData.undeletable) {\n            const buttonTdEl = document.createElement('td');\n            const buttonEl = document.createElement('we-button');\n            buttonEl.classList.add('o_we_select_remove_option', 'o_we_link', 'o_we_text_danger', 'fa', 'fa-fw', 'fa-minus');\n            buttonEl.dataset.removeOption = id;\n            buttonTdEl.appendChild(buttonEl);\n            trEl.appendChild(buttonTdEl);\n        }\n        this.listTable.appendChild(trEl);\n    },\n    /**\n     * @override\n     */\n    _getFocusableElement() {\n        return this.listTable.querySelector('input');\n    },\n    /**\n     * @private\n     */\n    _makeListItemsSortable() {\n        if (this.el.dataset.unsortable) {\n            return;\n        }\n        this.bindedSortable = this.call(\n            \"sortable\",\n            \"create\",\n            {\n                ref: { el: this.listTable },\n                elements: \"tr\",\n                followingElementClasses: [\"opacity-50\"],\n                handle: \".o_we_drag_handle\",\n                onDrop: () => this._notifyCurrentState(),\n                applyChangeOnDrop: true,\n            },\n        ).enable();\n    },\n    /**\n     * @private\n     * @param {Boolean} [preview]\n     */\n    _notifyCurrentState(preview = false) {\n        const isIdModeName = this.el.dataset.idMode === \"name\" || !this.isCustom;\n        const trimmed = (str) => str.trim().replace(/\\s+/g, \" \");\n        const values = [...this.listTable.querySelectorAll('.o_we_list_record_name input')].map(el => {\n            const id = trimmed(isIdModeName ? el.name : el.value);\n            return Object.assign({\n                id: /^-?[0-9]{1,15}$/.test(id) ? parseInt(id) : id,\n                name: trimmed(el.value),\n                display_name: trimmed(el.value),\n            }, el.dataset);\n        });\n        if (this.hasDefault) {\n            const checkboxes = [...this.listTable.querySelectorAll('we-button.o_we_checkbox_wrapper.active')];\n            this.selected = checkboxes.map(el => {\n                const input = el.parentElement.previousSibling.firstChild;\n                const id = trimmed(isIdModeName ? input.name : input.value);\n                return /^-?[0-9]{1,15}$/.test(id) ? parseInt(id) : id;\n            });\n            values.forEach(v => {\n                // Elements not toggleable are considered as always selected.\n                // We have to check that it is equal to the string 'true'\n                // because this information comes from the dataset.\n                v.selected = this.selected.includes(v.id) || v.notToggleable === 'true';\n            });\n        }\n        this._value = JSON.stringify(values);\n        if (preview) {\n            this._onUserValuePreview();\n        } else {\n            this._onUserValueChange();\n        }\n        if (!this.createWidget && !this.isCustom) {\n            this._reloadSelectDropdown(values);\n        }\n    },\n    /**\n     * @private\n     * @param {Array} currentValues\n     */\n    _reloadSelectDropdown(currentValues) {\n        this.selectMenuEl.innerHTML = '';\n        this.records.forEach(el => {\n            if (!currentValues.find(v => v.id === el.id)) {\n                const option = document.createElement('we-button');\n                option.classList.add('o_we_list_add_existing');\n                option.dataset.addOption = el.id;\n                option.dataset.noPreview = 'true';\n                const divEl = document.createElement('div');\n                divEl.textContent = el.display_name;\n                option.appendChild(divEl);\n                this.selectMenuEl.appendChild(option);\n            }\n        });\n        if (!this.selectMenuEl.children.length) {\n            const title = document.createElement('we-title');\n            title.textContent = _t(\"No more records\");\n            this.selectMenuEl.appendChild(title);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAddCustomItemClick() {\n        const recordData = {};\n        if (this.el.dataset.newElementsNotToggleable) {\n            recordData.notToggleable = true;\n        }\n        this._addItemToTable(undefined, this.el.dataset.defaultValue, recordData);\n        this._notifyCurrentState();\n        // Scroll to the new list element.\n        this.el.querySelector('tr:last-child')\n            .scrollIntoView({behavior: 'smooth', block: 'nearest'});\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAddExistingItemClick(ev) {\n        const value = ev.currentTarget.dataset.addOption;\n        this._addItemToTable(value, ev.currentTarget.textContent);\n        this._notifyCurrentState();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAddItemSelectClick(ev) {\n        ev.currentTarget.querySelector('we-toggler').classList.toggle('active');\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAddItemCheckboxClick: function (ev) {\n        const isActive = ev.currentTarget.classList.contains('active');\n        if (this.hasDefault === 'unique') {\n            this.listTable.querySelectorAll('we-button.o_we_checkbox_wrapper.active').forEach(el => el.classList.remove('active'));\n        }\n        ev.currentTarget.classList.toggle('active', !isActive);\n        this._notifyCurrentState();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onListItemBlurInput(ev) {\n        const preview = ev.type === 'input';\n        if (preview || !this.el.contains(ev.relatedTarget) || this.el.dataset.renderOnInputBlur) {\n            // We call the function below only if the element that recovers the\n            // focus after this blur is not an element of the we-list or if it\n            // is an input event (preview). This allows to use the TAB key to go\n            // from one input to another in the list. This behavior can be\n            // cancelled if the widget has reloadOnInputBlur = \"true\" in its\n            // dataset.\n            const timeSinceMousedown = ev.timeStamp - this.mousedownTime;\n            if (timeSinceMousedown < 500) {\n                // Without this \"setTimeOut\", \"click\" events are not triggered when\n                // clicking directly on a \"we-button\" of the \"we-list\" without first\n                // focusing out the input.\n                setTimeout(() => {\n                    this._notifyCurrentState(preview);\n                }, 500);\n            } else {\n                this._notifyCurrentState(preview);\n            }\n        }\n    },\n    /**\n     * @private\n     */\n    _onWeListMousedown(ev) {\n        this.mousedownTime = ev.timeStamp;\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onRemoveItemClick(ev) {\n        const minElements = this.el.dataset.allowEmpty ? 0 : 1;\n        if (ev.target.closest('table').querySelectorAll('tr').length > minElements) {\n            ev.target.closest('tr').remove();\n            this._notifyCurrentState();\n        }\n    },\n    /**\n     * @override\n     */\n    _onUserValueNotification(ev) {\n        const { widget, previewMode, prepare } = ev.data;\n        if (widget && widget === this.createWidget) {\n            if (widget.options.createMethod && widget.getValue(widget.options.createMethod)) {\n                return this._super(ev);\n            }\n            ev.stopPropagation();\n            if (previewMode) {\n                return;\n            }\n            prepare();\n            const recordData = JSON.parse(widget.getMethodsParams('addRecord').recordData);\n            const { id, display_name } = recordData;\n            delete recordData.id;\n            delete recordData.display_name;\n            this._addItemToTable(id, display_name, recordData);\n            this._notifyCurrentState();\n        }\n        return this._super(ev);\n    },\n});\n\nconst RangeUserValueWidget = UnitUserValueWidget.extend({\n    tagName: 'we-range',\n    events: {\n        'change input': '_onInputChange',\n        'input input': '_onInputInput',\n    },\n\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.input = document.createElement('input');\n        this.input.type = \"range\";\n        let min = this.el.dataset.min && parseFloat(this.el.dataset.min) || 0;\n        let max = this.el.dataset.max && parseFloat(this.el.dataset.max) || 100;\n        const step = this.el.dataset.step && parseFloat(this.el.dataset.step) || 1;\n        this.displayValue = this.el.dataset.displayRangeValue;\n        if (min > max) {\n            [min, max] = [max, min];\n            this.input.classList.add('o_we_inverted_range');\n        }\n        this._setInputAttributes(min, max, step);\n        this.containerEl.appendChild(this.input);\n        if (this.displayValue) {\n            this.outputEl = document.createElement('output');\n            this.outputEl.classList.add('ms-2');\n            this.containerEl.appendChild(this.outputEl);\n        }\n\n        this._onInputChange = debounce(this._onInputChange, 100);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    loadMethodsData(validMethodNames) {\n        this._super(...arguments);\n        for (const methodName of this._methodsNames) {\n            const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n            if (possibleValues.length > 1) {\n                this._setInputAttributes(0, possibleValues.length - 1, 1);\n                break;\n            }\n        }\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        await this._super(...arguments);\n        const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n        const inputValue = possibleValues.length > 1 ? possibleValues.indexOf(value) : this._value;\n        this.input.value = inputValue;\n        if (this.displayValue) {\n            this._computeDisplayValue(inputValue);\n        }\n    },\n    /**\n     * @override\n     */\n    getValue(methodName) {\n        const value = this._super(...arguments);\n        const possibleValues = this._methodsParams.optionsPossibleValues[methodName];\n        return possibleValues.length > 1 ? possibleValues[+value] : value;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onInputChange(ev) {\n        this._value = ev.target.value;\n        this._onUserValueChange(ev);\n    },\n    /**\n     * @private\n     * @param {string} inputValue\n     */\n    _computeDisplayValue(inputValue) {\n        if (this.el.dataset.toRatio) {\n            const inputValueAsNumber = Number(inputValue);\n            const ratio = inputValueAsNumber >= 0 ? 1 + inputValueAsNumber : 1 / (1 - inputValueAsNumber);\n            this.outputEl.value = `${ratio.toFixed(2)}x`;\n        } else if (this.el.dataset.displayRangeValueUnit) {\n            this.outputEl.value = inputValue + this.el.dataset.displayRangeValueUnit;\n        } else {\n            this.outputEl.value = inputValue;\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInputInput(ev) {\n        this._value = ev.target.value;\n        if (this.displayValue) {\n            this._computeDisplayValue(this._value);\n        }\n        this._onUserValuePreview(ev);\n    },\n    /**\n     * @private\n     */\n    _setInputAttributes(min, max, step) {\n        this.input.setAttribute('min', min);\n        this.input.setAttribute('max', max);\n        this.input.setAttribute('step', step);\n    },\n});\n\nconst SelectPagerUserValueWidget = SelectUserValueWidget.extend({\n    className: (SelectUserValueWidget.prototype.className || '') + ' o_we_select_pager',\n    events: Object.assign({}, SelectUserValueWidget.prototype.events, {\n        'click .o_pager_nav_btn': '_onClickScrollPage',\n        'click .o_pager_nav_angle': '_onClickCloseMenu',\n    }),\n    /**\n     * @override\n     */\n    async start() {\n        const _super = this._super.bind(this);\n\n        await _super(...arguments);\n        this.menuEl.classList.add('o_we_has_pager', 'position-fixed', 'top-0', 'end-0', 'z-1', 'rounded-0');\n        this.menuTogglerEl.classList.add('o_we_toggler_pager');\n\n        this.pagerContainerEl = this.el.querySelector('.o_pager_container');\n        this.__onScroll = throttleForAnimation(this._onScroll.bind(this));\n        this.pagerContainerEl.addEventListener('scroll', this.__onScroll);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.pagerContainerEl.removeEventListener('scroll', this.__onScroll);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * We never try to adjust the position for selection with pagers as they\n     * are fullscreen.\n     *\n     * @override\n     */\n    _adjustDropdownPosition() {\n        return;\n    },\n    /**\n     * @override\n     */\n    _shouldIgnoreClick(ev) {\n        return !!ev.target.closest('.o_pager_nav') || this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Scrolls to the requested section.\n     *\n     * @private\n     */\n    _onClickScrollPage(ev) {\n        const navButtonEl = ev.currentTarget;\n        const attribute = navButtonEl.dataset.scrollTo;\n        const destinationOffset = this.menuEl.querySelector('.' + attribute).offsetTop;\n\n        const pagerNavEl = this.menuEl.querySelector('.o_pager_nav');\n        this.pagerContainerEl.scrollTop = destinationOffset - pagerNavEl.offsetHeight;\n    },\n    /**\n     * @private\n     */\n    _onClickCloseMenu(ev) {\n        this.close();\n    },\n    /**\n     * @private\n     */\n    _onScroll(ev) {\n        const pagerContainerHeight = this.pagerContainerEl.getBoundingClientRect().height;\n        // The threshold for when a menu element is defined as 'active' is half\n        // of the container's height. This has a drawback as if a section\n        // is too small it might never get `active` if it's the last section.\n        const threshold = this.pagerContainerEl.scrollTop + (pagerContainerHeight / 2);\n        const anchorElements = this.menuEl.querySelectorAll('[data-scroll-to]');\n        for (const anchorEl of anchorElements) {\n            const destination = anchorEl.getAttribute('data-scroll-to');\n            const sectionEl = this.menuEl.querySelector(`.${destination}`);\n            const nextSectionEl = sectionEl.nextElementSibling;\n            anchorEl.classList.toggle('active', sectionEl.offsetTop < threshold &&\n            (!nextSectionEl || nextSectionEl.offsetTop > threshold));\n        }\n    }\n});\n\nconst Many2oneUserValueWidget = SelectUserValueWidget.extend({\n    className: (SelectUserValueWidget.prototype.className || '') + ' o_we_many2one',\n    events: Object.assign({}, SelectUserValueWidget.prototype.events, {\n        'input .o_we_m2o_search input': '_onSearchInput',\n        'keydown .o_we_m2o_search input': '_onSearchKeydown',\n        'click .o_we_m2o_search_more': '_onSearchMoreClick',\n    }),\n    // Data-attributes that will be read into `this.options` on init and not\n    // transfered to inner buttons.\n    // `domain` is the static part of the domain used in searches, not\n    // depending on already selected ids and other filters.\n    configAttributes: [\n        \"model\", \"fields\", \"limit\", \"domain\",\n        \"callWith\", \"createMethod\", \"filterInModel\", \"filterInField\", \"nullText\",\n        \"defaultMessage\",\n    ],\n\n    /**\n     * @override\n     */\n    init(parent, title, options, $target) {\n        this.afterSearch = [];\n        this.displayNameCache = {};\n        const {dataAttributes} = options;\n        Object.assign(options, {\n            limit: '5',\n            fields: '[]',\n            domain: '[]',\n            callWith: 'id',\n        });\n        this.configAttributes.forEach(attr => {\n            if (dataAttributes.hasOwnProperty(attr)) {\n                options[attr] = dataAttributes[attr];\n                delete dataAttributes[attr];\n            }\n        });\n        options.limit = parseInt(options.limit);\n        options.fields = JSON.parse(options.fields);\n        if (!options.fields.includes('display_name')) {\n            options.fields.push('display_name');\n        }\n        options.domain = JSON.parse(options.domain);\n        options.domainComponents = {};\n        options.nullText = $target[0].dataset.nullText ||\n            JSON.parse($target[0].dataset.oeContactOptions || '{}')['null_text'];\n\n        this.orm = serviceCached(this.bindService(\"orm\"));\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        this.inputEl = document.createElement('input');\n        this.inputEl.setAttribute('placeholder', _t(\"Search for records...\"));\n        const searchEl = document.createElement('div');\n        searchEl.classList.add('o_we_m2o_search');\n        searchEl.appendChild(this.inputEl);\n        this.menuEl.appendChild(searchEl);\n\n        this.searchMore = document.createElement('div');\n        this.searchMore.classList.add('o_we_m2o_search_more');\n        this.searchMore.textContent = _t(\"Search more...\");\n        this.searchMore.title = _t(\"Search to show more records\");\n\n        if (this.options.createMethod) {\n            this.createInput = new InputUserValueWidget(this, undefined, {\n                classes: ['o_we_large'],\n                dataAttributes: { noPreview: 'true' },\n            }, this.$target);\n            this.createButton = new ButtonUserValueWidget(this, undefined, {\n                classes: ['flex-grow-0'],\n                dataAttributes: {\n                    noPreview: 'true',\n                    [this.options.createMethod]: '', // Value through getValue.\n                },\n                childNodes: [document.createTextNode(_t(\"Create\"))],\n            }, this.$target);\n            // Override isActive so it doesn't show up in toggler\n            this.createButton.isActive = () => false;\n\n            await Promise.all([\n                this.createInput.appendTo(document.createDocumentFragment()),\n                this.createButton.appendTo(document.createDocumentFragment()),\n            ]);\n            this.registerSubWidget(this.createInput);\n            this.registerSubWidget(this.createButton);\n            this.createWidget = _buildRowElement('', {\n                classes: ['o_we_full_row', 'o_we_m2o_create', 'p-1'],\n                childNodes: [this.createInput.el, this.createButton.el],\n            });\n        }\n\n        return this._search('');\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        await this._super(...arguments);\n        if (this.menuTogglerEl.textContent === this.PLACEHOLDER_TEXT.toString()) {\n            // The currently selected value is not present in the search, need to read\n            // its display name.\n            if (value !== '') {\n                // FIXME: value may not be an id if callWith is specified!\n                this.menuTogglerEl.textContent = await this._getDisplayName(parseInt(value));\n            } else {\n                this.menuTogglerEl.textContent = this.options.defaultMessage || _t(\"Choose a record...\");\n            }\n        }\n    },\n    /**\n     * @override\n     */\n    getValue(methodName) {\n        if (methodName === this.options.createMethod && this.createInput) {\n            return this.createInput._value;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Prevents double widget instanciation for we-buttons that have been\n     * created manually by _search (container widgets will have their innner\n     * html searched for userValueWidgets to instanciate during option startup)\n     *\n     * @override\n     */\n    isContainer() {\n        return false;\n    },\n    /**\n     * @override\n     */\n    open() {\n        if (this.createInput) {\n            this.createInput.setValue('');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Updates the domain with defined inclusive filter to make sure that only\n     * records that are linked to specific records are retrieved.\n     * Filtering-in is configured with\n     *   * a `filterInModel` attribute, the linked model\n     *   * a `filterInField` attribute, field of the linked model holding\n     *   allowed values for this widget\n     *\n     * @param {integer[]} linkedRecordsIds\n     * @returns {Promise}\n     */\n    async setFilterInDomainIds(linkedRecordsIds) {\n        const allowedIds = new Set();\n        if (linkedRecordsIds) {\n            const parentRecordsData = await this.orm.searchRead(\n                this.options.filterInModel,\n                [['id', 'in', linkedRecordsIds]],\n                [this.options.filterInField]\n            );\n            parentRecordsData.forEach(record => {\n                record[this.options.filterInField].forEach(item => allowedIds.add(item));\n            });\n        }\n        if (allowedIds.size) {\n            this.options.domainComponents.filterInModel = ['id', 'in', [...allowedIds]];\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Searches the database for corresponding records and updates the dropdown\n     *\n     * @private\n     */\n    async _search(needle) {\n        const recTuples = await this.orm.call(this.options.model, \"name_search\", [], {\n            name: needle,\n            args: (await this._getSearchDomain()).concat(\n                Object.values(this.options.domainComponents).filter(item => item !== null)\n            ),\n            operator: \"ilike\",\n            limit: this.options.limit + 1,\n        });\n        const records = await this.orm.read(\n            this.options.model,\n            recTuples.map(([id, _name]) => id),\n            this.options.fields\n        );\n        // Remove select options.\n        this._userValueWidgets.filter(widget => {\n            return widget instanceof ButtonUserValueWidget &&\n                !widget.isDestroyed() &&\n                widget.el.parentElement.matches('we-selection-items');\n        }).forEach(button => {\n            if (button.isPreviewed()) {\n                button.notifyValueChange('reset');\n            }\n            button.destroy();\n        });\n        this._userValueWidgets = this._userValueWidgets.filter(widget => !widget.isDestroyed());\n        if (this.options.nullText &&\n                this.options.nullText.toLowerCase().includes(needle.toLowerCase())) {\n            // Beware of RPC cache.\n            if (!records.length || records[0].id) {\n                records.unshift({id: 0, name: this.options.nullText, display_name: this.options.nullText});\n            }\n        }\n        records.forEach(record => {\n            this.displayNameCache[record.id] = record.display_name;\n        });\n\n        await Promise.all(records.slice(0, this.options.limit).map(async record => {\n            // Copy over the data-attributes from the main element, and default the value\n            // to the callWith field of the record so that if it's a method, it will\n            // be called with that value\n            const buttonDataAttributes = Object.assign({}, this.options.dataAttributes);\n            Object.keys(buttonDataAttributes).forEach(key => {\n                buttonDataAttributes[key] = buttonDataAttributes[key] || record[this.options.callWith];\n            });\n            // REMARK: this syntax is very similar to React.createComponent, maybe we could\n            // write a transformer like there is for JSX?\n            const buttonWidget = new ButtonUserValueWidget(this, undefined, {\n                dataAttributes: Object.assign({recordData: JSON.stringify(record)}, buttonDataAttributes),\n                childNodes: [document.createTextNode(record.display_name)],\n            }, this.$target);\n            this.registerSubWidget(buttonWidget);\n            await buttonWidget.appendTo(this.menuEl);\n            if (this._methodsNames) {\n                buttonWidget.loadMethodsData(this._methodsNames);\n            }\n        }));\n        // Load methodsData for new buttons if possible. It will not be possible\n        // when the widget is first created (as this._methodsNames will be undefined)\n        // but the snippetOption lifecycle will load the methods data explicitely\n        // just after creating the widget\n        if (this._methodsNames) {\n            this._methodsNames.forEach(methodName => {\n                this.setValue(this._value, methodName);\n            });\n        }\n\n        const hasMore = records.length > this.options.limit;\n        if (hasMore) {\n            this.menuEl.appendChild(this.searchMore);\n            this.searchMore.classList.remove('d-none');\n        } else {\n            this.searchMore.classList.add('d-none');\n        }\n\n        if (this.createWidget) {\n            this.menuEl.appendChild(this.createWidget);\n        }\n\n        this.waitingForSearch = false;\n        this.afterSearch.forEach(cb => cb());\n        this.afterSearch = [];\n        if (this.options.nullText && !this.getValue()) {\n            this.setValue(0);\n        }\n    },\n    /**\n     * Returns the domain to use for the search.\n     *\n     * @private\n     */\n    async _getSearchDomain() {\n        return this.options.domain;\n    },\n    /**\n     * Returns the display name for a given record.\n     *\n     * @private\n     */\n    async _getDisplayName(recordId) {\n        if (!this.displayNameCache.hasOwnProperty(recordId)) {\n            this.displayNameCache[recordId] = (await this.orm.read(this.options.model, [recordId], ['display_name']))[0].display_name;\n        }\n        return this.displayNameCache[recordId];\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onClick(ev) {\n        // Prevent dropdown from closing if you click on the search or has_more\n        if (ev.target.closest('.o_we_m2o_search_more, .o_we_m2o_search, .o_we_m2o_create') &&\n                !ev.target.closest('we-button')) {\n            ev.stopPropagation();\n            return;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Handles changes to the search bar.\n     *\n     * @private\n     */\n    _onSearchInput(ev) {\n        // maybe there is a concurrency primitive we can use instead of manual record-keeping?\n        // Basically we want to queue the enter action to go after the current search if there\n        // is one that is ongoing (ie currently waiting for the debounce or RPC)\n        clearTimeout(this.searchIntent);\n        this.waitingForSearch = true;\n        this.searchIntent = setTimeout(() => {\n            this._search(ev.target.value);\n        }, 500);\n    },\n    /**\n     * Selects the first option when pressing enter in the search input.\n     *\n     * @private\n     */\n    _onSearchKeydown(ev) {\n        if (ev.key !== \"Enter\") {\n            return;\n        }\n        const action = () => {\n            const firstButton = this.menuEl.querySelector(':scope > we-button');\n            if (firstButton) {\n                firstButton.click();\n            }\n        };\n        if (this.waitingForSearch) {\n            this.afterSearch.push(action);\n        } else {\n            action();\n        }\n    },\n    /**\n     * Focuses the search input when clicking on the \"Search more...\" button.\n     *\n     * @private\n     */\n    _onSearchMoreClick(ev) {\n        this.inputEl.focus();\n    },\n    /**\n     * @override\n     */\n    _onUserValueNotification(ev) {\n        const { widget } = ev.data;\n        if (widget && widget === this.createInput) {\n            ev.stopPropagation();\n            return;\n        }\n        if (widget && widget === this.createButton) {\n            // When the create button is clicked, make sure the text\n            // value is restored from the actual input element because\n            // it might have been removed when hovering existing tags.\n            // TODO review this, there is probably better to do\n            this.createInput._value = this.createInput.el.querySelector('input').value;\n            if (!this.createInput._value) {\n                ev.stopPropagation();\n            }\n            return;\n        }\n        if (widget !== this.createButton && this.createInput) {\n            this.createInput._value = '';\n        }\n        return this._super(ev);\n    },\n});\n\nconst Many2manyUserValueWidget = UserValueWidget.extend({\n    configAttributes: ['model', 'recordId', 'm2oField', 'createMethod', 'fakem2m', 'filterIn'],\n\n    /**\n     * @override\n     */\n    init(parent, title, options, $target) {\n        const { dataAttributes } = options;\n        this.configAttributes.forEach(attr => {\n            if (dataAttributes.hasOwnProperty(attr)) {\n                options[attr] = dataAttributes[attr];\n                delete dataAttributes[attr];\n            }\n        });\n        this.filterIn = options.filterIn !== undefined;\n        if (this.filterIn) {\n            // Transfer filter-in values to child m2o.\n            dataAttributes.filterInModel = options.model;\n            dataAttributes.filterInField = options.m2oField;\n        }\n        this.orm = this.bindService(\"orm\");\n        this.fields = this.bindService(\"field\");\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async willStart() {\n        await this._super(...arguments);\n        // If the widget does not have a real m2m field in the database\n        // We do not need to fetch anything from the DB\n        if (this.options.fakem2m) {\n            this.m2oModel = this.options.model;\n            return;\n        }\n        const { model, recordId, m2oField } = this.options;\n        const [record] = await this.orm.read(model, [parseInt(recordId)], [m2oField]);\n        const selectedRecordIds = record[m2oField];\n        // TODO: handle no record\n        const modelData = await this.fields.loadFields(model, { fieldNames: [m2oField] });\n        // TODO: simultaneously fly both RPCs\n        this.m2oModel = modelData[m2oField].relation;\n        this.m2oName = modelData[m2oField].field_description; // Use as string attr?\n\n        const selectedRecords = await this.orm.read(this.m2oModel, selectedRecordIds, ['display_name']);\n        // TODO: reconcile the fact that this widget sets its own initial value\n        // instead of it coming through setValue(_computeWidgetState)\n        this._value = JSON.stringify(selectedRecords);\n    },\n    /**\n     * @override\n     */\n    async start() {\n        this.el.classList.add('o_we_m2m');\n        const m2oDataAttributes = Object.entries(this.options.dataAttributes).filter(([attrName]) => {\n            return Many2oneUserValueWidget.prototype.configAttributes.includes(attrName);\n        });\n        m2oDataAttributes.push(\n            ['model', this.m2oModel],\n            ['addRecord', ''],\n            ['createMethod', this.options.createMethod],\n        );\n        // Don't register this one as a subWidget because it will be a subWidget\n        // of the listWidget\n        this.createWidget = new Many2oneUserValueWidget(null, undefined, {\n            dataAttributes: Object.fromEntries(m2oDataAttributes),\n        }, this.$target);\n\n        this.listWidget = registerUserValueWidget('we-list', this, undefined, {\n            dataAttributes: { unsortable: 'true', notEditable: 'true', allowEmpty: 'true' },\n            createWidget: this.createWidget,\n        }, this.$target);\n        await this.listWidget.appendTo(this.containerEl);\n\n        // Make this.el the select's offsetParent so the we-selection-items has\n        // the correct width\n        this.listWidget.el.querySelector('we-select').style.position = 'static';\n        this.el.style.position = 'relative';\n    },\n    /**\n     * Only allow to fetch/select records which are linked (via `m2oField`) to the\n     * specified records.\n     *\n     * @param {integer[]} linkedRecordsIds\n     * @returns {Promise}\n     * @see Many2oneUserValueWidget.setFilterInDomainIds\n     */\n    async setFilterInDomainIds(linkedRecordsIds) {\n        if (this.filterIn) {\n            return this.listWidget.createWidget.setFilterInDomainIds(linkedRecordsIds);\n        }\n    },\n    /**\n     * @override\n     */\n    loadMethodsData(validMethodNames, ...rest) {\n        // TODO: check that addRecord is still needed.\n        this._super(['addRecord', ...validMethodNames], ...rest);\n        this._methodsNames = this._methodsNames.filter(name => name !== 'addRecord');\n    },\n    /**\n     * @override\n     */\n    setValue(value, methodName) {\n        if (methodName === this.options.createMethod) {\n            return this.createWidget.setValue(value, methodName);\n        }\n        if (!value) {\n            // TODO: why do we need this.\n            value = this._value;\n        }\n        this._super(value, methodName);\n        this.listWidget.setValue(this._value);\n    },\n    /**\n     * @override\n     */\n    getValue(methodName) {\n        return this.listWidget.getValue(methodName);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onUserValueNotification(ev) {\n        const { widget, previewMode } = ev.data;\n        if (!widget) {\n            return this._super(ev);\n        }\n        if (widget === this.listWidget) {\n            ev.stopPropagation();\n            this._value = widget._value;\n            this.notifyValueChange(previewMode);\n        }\n    },\n});\n\nconst userValueWidgetsRegistry = {\n    'we-button': ButtonUserValueWidget,\n    'we-checkbox': CheckboxUserValueWidget,\n    'we-select': SelectUserValueWidget,\n    'we-button-group': ButtonGroupUserValueWidget,\n    'we-input': InputUserValueWidget,\n    'we-multi': MultiUserValueWidget,\n    'we-colorpicker': ColorpickerUserValueWidget,\n    'we-datetimepicker': DatetimePickerUserValueWidget,\n    'we-datepicker': DatePickerUserValueWidget,\n    'we-list': ListUserValueWidget,\n    'we-imagepicker': ImagepickerUserValueWidget,\n    'we-videopicker': VideopickerUserValueWidget,\n    'we-range': RangeUserValueWidget,\n    'we-select-pager': SelectPagerUserValueWidget,\n    'we-many2one': Many2oneUserValueWidget,\n    'we-many2many': Many2manyUserValueWidget,\n};\n\n/**\n * Handles a set of options for one snippet. The registry returned by this\n * module contains the names of the specialized SnippetOptionWidget which can be\n * referenced thanks to the data-js key in the web_editor options template.\n */\nconst SnippetOptionWidget = publicWidget.Widget.extend({\n    tagName: 'we-customizeblock-option',\n    events: {\n        'click .o_we_collapse_toggler': '_onCollapseTogglerClick',\n    },\n    custom_events: {\n        'user_value_update': '_onUserValueUpdate',\n        'user_value_widget_critical': '_onUserValueWidgetCritical',\n    },\n    /**\n     * Indicates if the option should be displayed in the button group at the\n     * top of the options panel, next to the clone/remove button.\n     *\n     * @type {boolean}\n     */\n    isTopOption: false,\n    /**\n     * Indicates if the option should be the first one displayed in the button\n     * group at the top of the options panel, next to the clone/remove button.\n     *\n     * @type {boolean}\n     */\n    isTopFirstOption: false,\n    /**\n     * Forces the target to not be possible to remove. It will also hide the\n     * clone button.\n     *\n     * @type {boolean}\n     */\n    forceNoDeleteButton: false,\n    /**\n     * The option needs the handles overlay to be displayed on the snippet.\n     *\n     * @type {boolean}\n     */\n    displayOverlayOptions: false,\n    /**\n     * Forces the target to be duplicable.\n     *\n     * @type {boolean}\n     */\n    forceDuplicateButton: false,\n\n    /**\n     * The option `$el` is supposed to be the associated DOM UI element.\n     * The option controls another DOM element: the snippet it\n     * customizes, which can be found at `$target`. Access to the whole edition\n     * overlay is possible with `$overlay` (this is not recommended though).\n     *\n     * @constructor\n     */\n    init: function (parent, $uiElements, $target, $overlay, data, options) {\n        this._super.apply(this, arguments);\n\n        this.$originalUIElements = $uiElements;\n\n        this.$target = $target;\n        this.$overlay = $overlay;\n        this.data = data;\n        this.options = options;\n\n        this.className = 'snippet-option-' + this.data.optionName;\n\n        this.ownerDocument = this.$target[0].ownerDocument;\n\n        this._userValueWidgets = [];\n        this._actionQueues = new Map();\n\n        this.dialog = this.bindService(\"dialog\");\n    },\n    /**\n     * @override\n     */\n    willStart: async function () {\n        await this._super(...arguments);\n        return this._renderOriginalXML().then(uiFragment => {\n            this.uiFragment = uiFragment;\n        });\n    },\n    /**\n     * @override\n     */\n    renderElement: function () {\n        this._super(...arguments);\n        this.el.appendChild(this.uiFragment);\n        this.uiFragment = null;\n    },\n    /**\n     * Called when the parent edition overlay is covering the associated snippet\n     * (the first time, this follows the call to the @see start method).\n     *\n     * @abstract\n     * @returns {Promise|undefined}\n     */\n    async onFocus() {},\n    /**\n     * Called when the parent edition overlay is covering the associated snippet\n     * for the first time, when it is a new snippet dropped from the d&d snippet\n     * menu. Note: this is called after the start and onFocus methods.\n     *\n     * @abstract\n     * @param {Object} options\n     * @param {boolean} options.isCurrent\n     *        true if the main element has been built (so not when a child of\n     *        the main element has been built).\n     * @returns {Promise|undefined}\n     */\n    async onBuilt(options) {},\n    /**\n     * Called when the parent edition overlay is removed from the associated\n     * snippet (another snippet enters edition for example).\n     *\n     * @abstract\n     * @returns {Promise|undefined}\n     */\n    async onBlur() {},\n    /**\n     * Called when the associated snippet is the result of the cloning of\n     * another snippet (so `this.$target` is a cloned element).\n     *\n     * @abstract\n     * @param {Object} options\n     * @param {boolean} options.isCurrent\n     *        true if the associated snippet is a clone of the main element that\n     *        was cloned (so not a clone of a child of this main element that\n     *        was cloned)\n     */\n    onClone: function (options) {},\n    /**\n     * Called when the associated snippet is moved to another DOM location.\n     *\n     * @abstract\n     */\n    onMove: function () {},\n    /**\n     * Called when the associated snippet is about to be removed from the DOM.\n     *\n     * @abstract\n     * @returns {Promise|undefined}\n     */\n    onRemove: async function () {},\n    /**\n     * Called when the target is shown, only meaningful if the target was hidden\n     * at some point (typically used for 'invisible' snippets).\n     *\n     * @abstract\n     * @returns {Promise|undefined}\n     */\n    onTargetShow: async function () {},\n    /**\n     * Called when the target is hidden (typically used for 'invisible'\n     * snippets).\n     *\n     * @abstract\n     * @returns {Promise|undefined}\n     */\n    onTargetHide: async function () {},\n    /**\n     * Called when the template which contains the associated snippet is about\n     * to be saved.\n     *\n     * @abstract\n     * @return {Promise|undefined}\n     */\n    cleanForSave: async function () {},\n    /**\n     * Called when the associated snippet UI needs to be cleaned (e.g. from\n     * visual effects like previews).\n     * TODO this function will replace `cleanForSave` in the future.\n     *\n     * @abstract\n     * @return {Promise|undefined}\n     */\n    cleanUI: async function () {},\n    /**\n     * Adds the given widget to the known list of user value widgets\n     *\n     * @param {UserValueWidget} widget\n     */\n    registerSubWidget(widget) {\n        this._userValueWidgets.push(widget);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Default option method which allows to select one and only one class in\n     * the option classes set and set it on the associated snippet. The common\n     * case is having a select with each item having a `data-select-class`\n     * value allowing to choose the associated class, or simply an unique\n     * checkbox to allow toggling a unique class.\n     *\n     * @param {boolean|string} previewMode\n     *        - truthy if the option is enabled for preview or if leaving it (in\n     *          that second case, the value is 'reset')\n     *        - false if the option should be activated for good\n     * @param {string} widgetValue\n     * @param {Object} params\n     * @returns {Promise|undefined}\n     */\n    selectClass: function (previewMode, widgetValue, params) {\n        for (const classNames of params.possibleValues) {\n            if (classNames) {\n                this.$target[0].classList.remove(...classNames.trim().split(/\\s+/g));\n            }\n        }\n        if (widgetValue) {\n            this.$target[0].classList.add(...widgetValue.trim().split(/\\s+/g));\n        }\n    },\n    /**\n     * Default option method which allows to select a value and set it on the\n     * associated snippet as a data attribute. The name of the data attribute is\n     * given by the attributeName parameter.\n     *\n     * @param {boolean} previewMode - @see this.selectClass\n     * @param {string} widgetValue\n     * @param {Object} params\n     * @returns {Promise|undefined}\n     */\n    selectDataAttribute: function (previewMode, widgetValue, params) {\n        const value = this._selectAttributeHelper(widgetValue, params);\n        this.$target[0].dataset[params.attributeName] = value;\n    },\n    /**\n     * Default option method which allows to select a value and set it on the\n     * associated snippet as an attribute. The name of the attribute is\n     * given by the attributeName parameter.\n     *\n     * @param {boolean} previewMode - @see this.selectClass\n     * @param {string} widgetValue\n     * @param {Object} params\n     * @returns {Promise|undefined}\n     */\n    selectAttribute: function (previewMode, widgetValue, params) {\n        const value = this._selectAttributeHelper(widgetValue, params);\n        if (value) {\n            this.$target[0].setAttribute(params.attributeName, value);\n        } else {\n            this.$target[0].removeAttribute(params.attributeName);\n        }\n    },\n    /**\n     * Default option method which allows to select a value and set it on the\n     * associated snippet as a property. The name of the property is\n     * given by the propertyName parameter.\n     *\n     * @param {boolean} previewMode - @see this.selectClass\n     * @param {string} widgetValue\n     * @param {Object} params\n     */\n    selectProperty: function (previewMode, widgetValue, params) {\n        if (!params.propertyName) {\n            throw new Error('Property name missing');\n        }\n        const value = this._selectValueHelper(widgetValue, params);\n        this.$target[0][params.propertyName] = value;\n    },\n    /**\n     * Default option method which allows to select a value and set it on the\n     * associated snippet as a css style. The name of the css property is\n     * given by the cssProperty parameter.\n     *\n     * @param {boolean} previewMode - @see this.selectClass\n     * @param {string} widgetValue\n     * @param {Object} params\n     * @param {string} [params.forceStyle] if undefined, the method will not\n     *      set the inline style (and thus even remove it) if the item would\n     *      already have the given style without it (thanks to a CSS rule for\n     *      example). If defined (as a string), it acts as the \"priority\" param\n     *      of @see CSSStyleDeclaration.setProperty: it should be 'important' to\n     *      set the style as important or '' otherwise. Note that if forceStyle\n     *      is undefined, the style is set as important only if required to have\n     *      an effect.\n     * @returns {Promise|undefined}\n     */\n    selectStyle: async function (previewMode, widgetValue, params) {\n        // Disable all transitions for the duration of the method as many\n        // comparisons will be done on the element to know if applying a\n        // property has an effect or not. Also, changing a css property via the\n        // editor should not show any transition as previews would not be done\n        // immediately, which is not good for the user experience.\n        this.$target[0].classList.add('o_we_force_no_transition');\n        const _restoreTransitions = () => this.$target[0].classList.remove('o_we_force_no_transition');\n\n        if (params.cssProperty === 'background-color') {\n            this.$target.trigger('background-color-event', previewMode);\n        }\n\n        // Always reset the inline style first to not put inline style on an\n        // element which already have this style through css stylesheets.\n        let cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty];\n        for (const cssProp of cssProps) {\n            this.$target[0].style.setProperty(cssProp, '');\n        }\n        if (params.extraClass) {\n            this.$target.removeClass(params.extraClass);\n        }\n        // Plain color and gradient are mutually exclusive as background so in\n        // case we edit a background-color we also have to reset the gradient\n        // part of the background-image property (the opposite is handled by the\n        // fact that editing a gradient as background is done by calling this\n        // method with background-color as property too, so it is automatically\n        // reset anyway).\n        let bgImageParts = undefined;\n        if (params.withGradients && params.cssProperty === 'background-color') {\n            const styles = getComputedStyle(this.$target[0]);\n            bgImageParts = backgroundImageCssToParts(styles['background-image']);\n            delete bgImageParts.gradient;\n            this.$target[0].style.setProperty('background-image', '');\n            if (!widgetValue || widgetValue === 'false') {\n                // If no background-color is being set and there is an image,\n                // combine it with the current color combination's gradient.\n                const styleBgImageParts = backgroundImageCssToParts(styles['background-image']);\n                bgImageParts.gradient = styleBgImageParts.gradient;\n            }\n            const combined = backgroundImagePartsToCss(bgImageParts);\n            applyCSS.call(this, 'background-image', combined, styles);\n        }\n\n        // Only allow to use a color name as a className if we know about the\n        // other potential color names (to remove) and if we know about a prefix\n        // (otherwise we suppose that we should use the actual related color).\n        // Note: color combinations classes are handled by a dedicated method,\n        // as they can be combined with normal classes.\n        if (params.colorNames && params.colorPrefix) {\n            const colorNames = params.colorNames.filter(name => !weUtils.isColorCombinationName(name));\n            const classes = weUtils.computeColorClasses(colorNames, params.colorPrefix);\n            this.$target[0].classList.remove(...classes);\n\n            if (colorNames.includes(widgetValue)) {\n                const originalCSSValue = window.getComputedStyle(this.$target[0])[cssProps[0]];\n                const className = params.colorPrefix + widgetValue;\n                this.$target[0].classList.add(className);\n                if (originalCSSValue !== window.getComputedStyle(this.$target[0])[cssProps[0]]) {\n                    // If applying the class did indeed changed the css\n                    // property we are editing, nothing more has to be done.\n                    // (except adding the extra class)\n                    this.$target.addClass(params.extraClass);\n                    _restoreTransitions();\n                    return;\n                }\n                // Otherwise, it means that class probably does not exist,\n                // we remove it and continue. Especially useful for some\n                // prefixes which only work with some color names but not all.\n                this.$target[0].classList.remove(className);\n            }\n        }\n\n        const styles = window.getComputedStyle(this.$target[0]);\n\n        // At this point, the widget value is either a property/color name or\n        // an actual css property value. If it is a property/color name, we will\n        // apply a css variable as style value.\n        const htmlPropValue = weUtils.getCSSVariableValue(widgetValue);\n        if (htmlPropValue) {\n            widgetValue = `var(--${widgetValue})`;\n        }\n\n        // In case of background-color edition, we could receive a gradient, in\n        // which case the value has to be combined with the potential background\n        // image (real image).\n        if (params.withGradients && params.cssProperty === 'background-color' && weUtils.isColorGradient(widgetValue)) {\n            cssProps = ['background-image'];\n            bgImageParts.gradient = widgetValue;\n            widgetValue = backgroundImagePartsToCss(bgImageParts);\n\n            // Also force the background-color to transparent as otherwise it\n            // won't act as a \"gradient replacing the color combination\n            // background\" but be applied over it (which would be the opposite\n            // of what happens when editing the background color).\n            applyCSS.call(this, 'background-color', 'rgba(0, 0, 0, 0)', styles);\n        }\n\n        // replacing ', ' by ',' to prevent attributes with internal space separators from being split:\n        // eg: \"rgba(55, 12, 47, 1.9) 47px\" should be split as [\"rgba(55,12,47,1.9)\", \"47px\"]\n        const values = widgetValue.replace(/,\\s/g, ',').split(/\\s+/g);\n        while (values.length < cssProps.length) {\n            switch (values.length) {\n                case 1:\n                case 2: {\n                    values.push(values[0]);\n                    break;\n                }\n                case 3: {\n                    values.push(values[1]);\n                    break;\n                }\n                default: {\n                    values.push(values[values.length - 1]);\n                }\n            }\n        }\n\n        let hasUserValue = false;\n        const applyAllCSS = (values) => {\n            for (let i = cssProps.length - 1; i > 0; i--) {\n                hasUserValue = applyCSS.call(this, cssProps[i], values.pop(), styles) || hasUserValue;\n            }\n            hasUserValue = applyCSS.call(this, cssProps[0], values.join(' '), styles) || hasUserValue;\n        }\n\n        applyAllCSS([...values]);\n\n        function applyCSS(cssProp, cssValue, styles) {\n            if (typeof params.forceStyle !== 'undefined') {\n                this.$target[0].style.setProperty(cssProp, cssValue, params.forceStyle);\n                return true;\n            }\n\n            if (!weUtils.areCssValuesEqual(styles.getPropertyValue(cssProp), cssValue, cssProp, this.$target[0])) {\n                this.$target[0].style.setProperty(cssProp, cssValue);\n                // If change had no effect then make it important.\n                if (!params.preventImportant && !weUtils.areCssValuesEqual(\n                        styles.getPropertyValue(cssProp), cssValue, cssProp, this.$target[0])) {\n                    this.$target[0].style.setProperty(cssProp, cssValue, 'important');\n                }\n                return true;\n            }\n            return false;\n        }\n\n        if (params.extraClass) {\n            this.$target.toggleClass(params.extraClass, hasUserValue);\n            if (hasUserValue) {\n                // Might have changed because of the class.\n                for (const cssProp of cssProps) {\n                    this.$target[0].style.removeProperty(cssProp);\n                }\n                applyAllCSS(values);\n            }\n        }\n\n        _restoreTransitions();\n    },\n    /**\n     * Sets a color combination.\n     *\n     * @see this.selectClass for parameters\n     */\n    async selectColorCombination(previewMode, widgetValue, params) {\n        if (params.colorNames) {\n            const names = params.colorNames.filter(weUtils.isColorCombinationName);\n            const classes = weUtils.computeColorClasses(names);\n            this.$target[0].classList.remove(...classes);\n\n            if (widgetValue) {\n                this.$target[0].classList.add('o_cc', `o_cc${widgetValue}`);\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Override the helper method to search inside the $target element instead\n     * of the UI item element.\n     *\n     * @override\n     */\n    $: function () {\n        return this.$target.find.apply(this.$target, arguments);\n    },\n    /**\n     * Closes all user value widgets.\n     */\n    closeWidgets: function () {\n        this._userValueWidgets.forEach(widget => widget.close());\n    },\n    /**\n     * @param {string} name\n     * @returns {UserValueWidget|null}\n     */\n    findWidget: function (name) {\n        for (const widget of this._userValueWidgets) {\n            if (widget.getName() === name) {\n                return widget;\n            }\n            const depWidget = widget.findWidget(name);\n            if (depWidget) {\n                return depWidget;\n            }\n        }\n        return null;\n    },\n    /**\n     * Sometimes, options may need to notify other options, even in parent\n     * editors. This can be done thanks to the 'option_update' event, which\n     * will then be handled by this function.\n     *\n     * @param {string} name - an identifier for a type of update\n     * @param {*} data\n     */\n    notify: function (name, data) {\n        // We prefer to avoid refactoring this notify mechanism to make it\n        // asynchronous because the upcoming conversion to owl might remove it.\n        if (name === 'target') {\n            this.setTarget(data);\n        }\n    },\n    /**\n     * Sometimes, an option is binded on an element but should in fact apply on\n     * another one. For example, elements which contain slides: we want all the\n     * per-slide options to be in the main menu of the whole snippet. This\n     * function allows to set the option's target.\n     *\n     * Note: the UI is not updated accordindly automatically.\n     *\n     * @param {jQuery} $target - the new target element\n     * @returns {Promise}\n     */\n    setTarget: function ($target) {\n        this.$target = $target;\n    },\n    /**\n     * Updates the UI. For widget update, @see _computeWidgetState.\n     *\n     * @param {boolean} [noVisibility=false]\n     *     If true, only update widget values and their UI, not their visibility\n     *     -> @see updateUIVisibility for toggling visibility only\n     * @param {boolean} [assetsChanged=false]\n     *     If true, widgets might prefer to _rerenderXML instead of calling\n     *     this super implementation\n     * @returns {Promise}\n     */\n    async updateUI({noVisibility, assetsChanged} = {}) {\n        // For each widget, for each of their option method, notify to the\n        // widget the current value they should hold according to the $target's\n        // current state, related for that method.\n        const proms = this._userValueWidgets.map(async widget => {\n            // Update widget value (for each method)\n            const methodsNames = widget.getMethodsNames();\n            for (const methodName of methodsNames) {\n                const params = widget.getMethodsParams(methodName);\n\n                let obj = this;\n                if (params.applyTo) {\n                    const $firstSubTarget = this.$(params.applyTo).eq(0);\n                    if (!$firstSubTarget.length) {\n                        continue;\n                    }\n                    obj = createPropertyProxy(this, '$target', $firstSubTarget);\n                }\n\n                const value = await this._computeWidgetState.call(obj, methodName, params);\n                if (value === undefined) {\n                    continue;\n                }\n                const normalizedValue = this._normalizeWidgetValue(value);\n                await widget.setValue(normalizedValue, methodName);\n            }\n        });\n        await Promise.all(proms);\n\n        if (!noVisibility) {\n            await this.updateUIVisibility();\n        }\n    },\n    /**\n     * Updates the UI visibility - @see _computeVisibility. For widget update,\n     * @see _computeWidgetVisibility.\n     *\n     * @returns {Promise}\n     */\n    updateUIVisibility: async function () {\n        const proms = this._userValueWidgets.map(async widget => {\n            const params = widget.getMethodsParams();\n\n            let obj = this;\n            if (params.applyTo) {\n                const $firstSubTarget = this.$(params.applyTo).eq(0);\n                if (!$firstSubTarget.length) {\n                    widget.toggleVisibility(false);\n                    return;\n                }\n                obj = createPropertyProxy(this, '$target', $firstSubTarget);\n            }\n\n            // Make sure to check the visibility of all sub-widgets. For\n            // simplicity and efficiency, those will be checked with main\n            // widgets params.\n            const allSubWidgets = [widget];\n            let i = 0;\n            while (i < allSubWidgets.length) {\n                allSubWidgets.push(...allSubWidgets[i]._userValueWidgets);\n                i++;\n            }\n            const proms = allSubWidgets.map(async widget => {\n                const show = await this._computeWidgetVisibility.call(obj, widget.getName(), params);\n                if (!show) {\n                    widget.toggleVisibility(false);\n                    return;\n                }\n\n                const dependencies = widget.getDependencies();\n\n                if (dependencies.length === 1 && dependencies[0] === 'fake') {\n                    widget.toggleVisibility(false);\n                    return;\n                }\n\n                const dependenciesData = [];\n                dependencies.forEach(depName => {\n                    const toBeActive = (depName[0] !== '!');\n                    if (!toBeActive) {\n                        depName = depName.substr(1);\n                    }\n\n                    const widget = this._requestUserValueWidgets(depName, true)[0];\n                    if (widget) {\n                        dependenciesData.push({\n                            widget: widget,\n                            toBeActive: toBeActive,\n                        });\n                    }\n                });\n                const dependenciesOK = !dependenciesData.length || dependenciesData.some(depData => {\n                    return (depData.widget.isActive() === depData.toBeActive);\n                });\n\n                widget.toggleVisibility(dependenciesOK);\n            });\n            return Promise.all(proms);\n        });\n\n        const showUI = await this._computeVisibility();\n        this.el.classList.toggle('d-none', !showUI);\n\n        await Promise.all(proms);\n\n        // Hide layouting elements which contains only hidden widgets\n        // TODO improve this, this is hackish to rely on DOM structure here.\n        // Layouting elements should be handled as widgets or other.\n        for (const el of this.$el.find('we-row')) {\n            const $userValueWidget = $(el).find('> div > .o_we_user_value_widget');\n            el.classList.toggle('d-none', $userValueWidget.length && !$userValueWidget.not('.d-none').length);\n        }\n        for (const el of this.$el.find('we-collapse')) {\n            const $el = $(el);\n            el.classList.toggle('d-none', $el.children().first().hasClass('d-none'));\n            const hasNoVisibleElInCollapseMenu = !$el.children().last().children().not('.d-none').length;\n            if (hasNoVisibleElInCollapseMenu) {\n                this._toggleCollapseEl(el, false);\n            }\n            el.querySelector('.o_we_collapse_toggler').classList.toggle('d-none', hasNoVisibleElInCollapseMenu);\n        }\n\n        return !this.displayOverlayOptions && showUI;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {UserValueWidget[]} widgets\n     * @returns {Promise<string>}\n     */\n    async _checkIfWidgetsUpdateNeedWarning(widgets) {\n        const messages = [];\n        for (const widget of widgets) {\n            const message = widget.getMethodsParams().warnMessage;\n            if (message) {\n                messages.push(message);\n            }\n        }\n        return messages.join(' ');\n    },\n    /**\n     * @private\n     * @param {UserValueWidget[]} widgets\n     * @returns {Promise<boolean|string>}\n     */\n    async _checkIfWidgetsUpdateNeedReload(widgets) {\n        return false;\n    },\n    /**\n     * @private\n     * @returns {Promise<boolean>|boolean}\n     */\n    _computeVisibility: async function () {\n        return true;\n    },\n    /**\n     * Returns the string value that should be hold by the widget which is\n     * related to the given method name.\n     *\n     * If the value is irrelevant for a method, it must return undefined.\n     *\n     * @private\n     * @param {string} methodName\n     * @param {Object} params\n     * @returns {Promise<string|undefined>|string|undefined}\n     */\n    _computeWidgetState: async function (methodName, params) {\n        switch (methodName) {\n            case 'selectClass': {\n                let maxNbClasses = 0;\n                let activeClassNames = '';\n                for (const classNames of params.possibleValues) {\n                    if (!classNames) {\n                        continue;\n                    }\n                    const classes = classNames.split(/\\s+/g);\n                    if (params.stateToFirstClass) {\n                        if (this.$target[0].classList.contains(classes[0])) {\n                            return classNames;\n                        } else {\n                            continue;\n                        }\n                    }\n\n                    if (classes.length >= maxNbClasses\n                            && classes.every(className => this.$target[0].classList.contains(className))) {\n                        maxNbClasses = classes.length;\n                        activeClassNames = classNames;\n                    }\n                }\n                return activeClassNames;\n            }\n            case 'selectAttribute':\n            case 'selectDataAttribute': {\n                const attrName = params.attributeName;\n                let attrValue;\n                if (methodName === 'selectAttribute') {\n                    attrValue = this.$target[0].getAttribute(attrName);\n                } else if (methodName === 'selectDataAttribute') {\n                    attrValue = this.$target[0].dataset[attrName];\n                }\n                attrValue = (attrValue || '').trim();\n                if (params.saveUnit && !params.withUnit) {\n                    attrValue = attrValue.split(/\\s+/g).map(v => v + params.saveUnit).join(' ');\n                }\n                return attrValue || params.attributeDefaultValue || '';\n            }\n            case 'selectStyle': {\n                let usedCC = undefined;\n                if (params.colorPrefix && params.colorNames) {\n                    for (const c of params.colorNames) {\n                        const className = weUtils.computeColorClasses([c], params.colorPrefix)[0];\n                        if (this.$target[0].classList.contains(className)) {\n                            if (weUtils.isColorCombinationName(c)) {\n                                usedCC = c;\n                                continue;\n                            }\n                            return c;\n                        }\n                    }\n                }\n\n                // Disable all transitions for the duration of the style check\n                // as we want to know the final value of a property to properly\n                // update the UI.\n                this.$target[0].classList.add('o_we_force_no_transition');\n\n                const styles = window.getComputedStyle(this.$target[0]);\n                const cssProps = weUtils.CSS_SHORTHANDS[params.cssProperty] || [params.cssProperty];\n                const borderWidthCssProps = weUtils.CSS_SHORTHANDS['border-width'];\n                const cssValues = cssProps.map(cssProp => {\n                    let value = styles.getPropertyValue(cssProp).trim();\n                    if (cssProp === 'box-shadow') {\n                        const inset = value.includes('inset');\n                        let values = value.replace(/,\\s/g, ',').replace('inset', '').trim().split(/\\s+/g);\n                        const color = values.find(s => !s.match(/^\\d/));\n                        values = values.join(' ').replace(color, '').trim();\n                        value = `${color} ${values}${inset ? ' inset' : ''}`;\n                    }\n                    if (borderWidthCssProps.includes(cssProp) && value.endsWith('px')) {\n                        // Rounding value up avoids zoom-in issues.\n                        // Zoom-out issues are not an expected use case.\n                        value = `${Math.ceil(parseFloat(value))}px`;\n                    }\n                    return value;\n                });\n                if (cssValues.length === 4 && weUtils.areCssValuesEqual(cssValues[3], cssValues[1], params.cssProperty, this.$target)) {\n                    cssValues.pop();\n                }\n                if (cssValues.length === 3 && weUtils.areCssValuesEqual(cssValues[2], cssValues[0], params.cssProperty, this.$target)) {\n                    cssValues.pop();\n                }\n                if (cssValues.length === 2 && weUtils.areCssValuesEqual(cssValues[1], cssValues[0], params.cssProperty, this.$target)) {\n                    cssValues.pop();\n                }\n\n                let value = cssValues.join(' ');\n                if (params.withGradients && params.cssProperty === 'background-color') {\n                    // Check if there is a gradient, in that case this is the\n                    // value to be returned, we normally do not allow color and\n                    // gradient at the same time (the option would remove one\n                    // if editing the other).\n                    const parts = backgroundImageCssToParts(styles['background-image']);\n                    if (parts.gradient) {\n                        value = parts.gradient;\n                    }\n                }\n\n                this.$target[0].classList.remove('o_we_force_no_transition');\n\n                if (params.cssProperty === 'background-color' && params.withCombinations) {\n                    if (usedCC) {\n                        const ccValue = weUtils.getCSSVariableValue(`o-cc${usedCC}-bg-gradient`).trim().replaceAll(\"'\", '')\n                            || weUtils.getCSSVariableValue(`o-cc${usedCC}-bg`).trim();\n                        if (weUtils.areCssValuesEqual(value, ccValue)) {\n                            // Prevent to consider that a color is used as CC\n                            // override in case that color is the same as the\n                            // one used in that CC.\n                            return '';\n                        }\n                    } else {\n                        const rgba = convertCSSColorToRgba(value);\n                        if (rgba && rgba.opacity < 0.001) {\n                            // Prevent to consider a transparent color is\n                            // applied as background unless it is to override a\n                            // CC. Simply allows to add a CC on a transparent\n                            // snippet in the first place.\n                            return '';\n                        }\n                    }\n                }\n                // When the default color is the target's \"currentColor\", the\n                // value should be handled correctly by the option.\n                if (value === \"currentColor\") {\n                    return styles.color;\n                }\n\n                return value;\n            }\n            case 'selectColorCombination': {\n                if (params.colorNames) {\n                    for (const c of params.colorNames) {\n                        if (!weUtils.isColorCombinationName(c)) {\n                            continue;\n                        }\n                        const className = weUtils.computeColorClasses([c])[0];\n                        if (this.$target[0].classList.contains(className)) {\n                            return c;\n                        }\n                    }\n                }\n                return '';\n            }\n        }\n    },\n    /**\n     * @private\n     * @param {string} widgetName\n     * @param {Object} params\n     * @returns {Promise<boolean>|boolean}\n     */\n    _computeWidgetVisibility: async function (widgetName, params) {\n        return true;\n    },\n    /**\n     * @private\n     * @param {HTMLElement} el\n     * @returns {Object}\n     */\n    _extraInfoFromDescriptionElement: function (el) {\n        return {\n            title: el.getAttribute('string'),\n            options: {\n                classes: el.classList,\n                dataAttributes: el.dataset,\n                tooltip: el.title,\n                placeholder: el.getAttribute('placeholder'),\n                childNodes: [...el.childNodes],\n            },\n        };\n    },\n    /**\n     * @private\n     * @param {*}\n     * @returns {string}\n     */\n    _normalizeWidgetValue: function (value) {\n        value = `${value}`.trim(); // Force to a trimmed string\n        value = normalizeCSSColor(value); // If is a css color, normalize it\n        return value;\n    },\n    /**\n     * @private\n     * @param {HTMLElement} uiFragment\n     * @returns {Promise}\n     */\n    _renderCustomWidgets: function (uiFragment) {\n        return Promise.resolve();\n    },\n    /**\n     * @private\n     * @param {HTMLElement} uiFragment\n     * @returns {Promise}\n     */\n    _renderCustomXML: function (uiFragment) {\n        return Promise.resolve();\n    },\n    /**\n     * @private\n     * @param {jQuery} [$xml] - default to original xml content\n     * @returns {Promise}\n     */\n    _renderOriginalXML: async function ($xml) {\n        const uiFragment = document.createDocumentFragment();\n        ($xml || this.$originalUIElements).clone(true).appendTo(uiFragment);\n\n        await this._renderCustomXML(uiFragment);\n\n        // Build layouting components first\n        for (const [itemName, build] of [['we-row', _buildRowElement], ['we-collapse', _buildCollapseElement]]) {\n            uiFragment.querySelectorAll(itemName).forEach(el => {\n                const infos = this._extraInfoFromDescriptionElement(el);\n                const groupEl = build(infos.title, infos.options);\n                el.parentNode.insertBefore(groupEl, el);\n                el.parentNode.removeChild(el);\n            });\n        }\n\n        // Load widgets\n        await this._renderXMLWidgets(uiFragment);\n        await this._renderCustomWidgets(uiFragment);\n\n        if (this.isDestroyed()) {\n            // TODO there is probably better to do. This case was found only in\n            // tours, where the editor is left before the widget are fully\n            // loaded (loadMethodsData doesn't work if the widget is destroyed).\n            return uiFragment;\n        }\n\n        const validMethodNames = [];\n        for (const key in this) {\n            validMethodNames.push(key);\n        }\n        this._userValueWidgets.forEach(widget => {\n            widget.loadMethodsData(validMethodNames);\n        });\n\n        return uiFragment;\n    },\n    /**\n     * @private\n     * @param {HTMLElement} parentEl\n     * @param {SnippetOptionWidget|UserValueWidget} parentWidget\n     * @returns {Promise}\n     */\n    _renderXMLWidgets: function (parentEl, parentWidget) {\n        const proms = [...parentEl.children].map(el => {\n            const widgetName = el.tagName.toLowerCase();\n            if (!userValueWidgetsRegistry.hasOwnProperty(widgetName)) {\n                return this._renderXMLWidgets(el, parentWidget);\n            }\n\n            const infos = this._extraInfoFromDescriptionElement(el);\n            const widget = registerUserValueWidget(widgetName, parentWidget || this, infos.title, infos.options, this.$target);\n            return widget.insertAfter(el).then(() => {\n                // Remove the original element afterwards as the insertion\n                // operation may move some of its inner content during\n                // widget start.\n                parentEl.removeChild(el);\n\n                if (widget.isContainer() && !widget.isDestroyed()) {\n                    return this._renderXMLWidgets(widget.el, widget);\n                }\n            });\n        });\n        return Promise.all(proms);\n    },\n    /**\n     * @private\n     * @param {...string} widgetNames\n     * @param {boolean} [allowParentOption=false]\n     * @returns {UserValueWidget[]}\n     */\n    _requestUserValueWidgets: function (...args) {\n        const widgetNames = args;\n        let allowParentOption = false;\n        const lastArg = args[args.length - 1];\n        if (typeof lastArg === 'boolean') {\n            widgetNames.pop();\n            allowParentOption = lastArg;\n        }\n\n        const widgets = [];\n        for (const widgetName of widgetNames) {\n            let widget = null;\n            this.trigger_up('user_value_widget_request', {\n                name: widgetName,\n                onSuccess: _widget => widget = _widget,\n                allowParentOption: allowParentOption,\n            });\n            if (widget) {\n                widgets.push(widget);\n            }\n        }\n        return widgets;\n    },\n    /**\n     * @private\n     * @param {function<Promise<jQuery>>} [callback]\n     * @returns {Promise}\n     */\n    _rerenderXML: async function (callback) {\n        this._userValueWidgets.forEach(widget => widget.destroy());\n        this._userValueWidgets = [];\n        this.$el.empty();\n\n        let $xml = undefined;\n        if (callback) {\n            $xml = await callback.call(this);\n        }\n\n        return this._renderOriginalXML($xml).then(uiFragment => {\n            this.$el.append(uiFragment);\n            return this.updateUI();\n        });\n    },\n    /**\n     * Activates the option associated to the given DOM element.\n     *\n     * @private\n     * @param {boolean|string} previewMode\n     *        - truthy if the option is enabled for preview or if leaving it (in\n     *          that second case, the value is 'reset')\n     *        - false if the option should be activated for good\n     * @param {UserValueWidget} widget - the widget which triggered the option change\n     * @returns {Promise}\n     */\n    _select: async function (previewMode, widget) {\n        let $applyTo = null;\n\n        if (previewMode === true) {\n            this.options.wysiwyg.odooEditor.automaticStepUnactive('preview_option');\n        }\n\n        // Call each option method sequentially\n        for (const methodName of widget.getMethodsNames()) {\n            const widgetValue = widget.getValue(methodName);\n            const params = widget.getMethodsParams(methodName);\n\n            if (params.applyTo) {\n                if (!$applyTo) {\n                    $applyTo = this.$(params.applyTo);\n                }\n                const proms = Array.from($applyTo).map((subTargetEl) => {\n                    const proxy = createPropertyProxy(this, '$target', $(subTargetEl));\n                    return this[methodName].call(proxy, previewMode, widgetValue, params);\n                });\n                await Promise.all(proms);\n            } else {\n                await this[methodName](previewMode, widgetValue, params);\n            }\n        }\n\n        if (previewMode === 'reset' || previewMode === false) {\n            this.options.wysiwyg.odooEditor.automaticStepActive('preview_option');\n        }\n\n        // We trigger the event on elements targeted by apply-to if any as\n        // this.$target could not be in an editable element while the elements\n        // targeted by apply-to are.\n        ($applyTo || this.$target).trigger('content_changed');\n    },\n    /**\n     * Used to handle attribute or data attribute value change\n     *\n     * @see this._selectValueHelper for parameters\n     */\n    _selectAttributeHelper(value, params) {\n        if (!params.attributeName) {\n            throw new Error('Attribute name missing');\n        }\n        return this._selectValueHelper(value, params);\n    },\n    /**\n     * Used to handle value of a select\n     *\n     * @param {string} value\n     * @param {Object} params\n     * @returns {string|undefined}\n     */\n    _selectValueHelper(value, params) {\n        if (params.saveUnit && !params.withUnit) {\n            // Values that come with an unit are saved without unit as\n            // data-attribute unless told otherwise.\n            value = value.split(params.saveUnit).join('');\n        }\n        if (params.extraClass) {\n            this.$target.toggleClass(params.extraClass, params.defaultValue !== value);\n        }\n        return value;\n    },\n    /**\n     * @private\n     * @param {HTMLElement} collapseEl\n     * @param {boolean|undefined} [show]\n     */\n    _toggleCollapseEl(collapseEl, show) {\n        collapseEl.classList.toggle('active', show);\n        collapseEl.querySelector('we-toggler.o_we_collapse_toggler').classList.toggle('active', show);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onCollapseTogglerClick(ev) {\n        const currentCollapseEl = ev.currentTarget.closest('we-collapse');\n        this._toggleCollapseEl(currentCollapseEl);\n        for (const collapseEl of currentCollapseEl.querySelectorAll('we-collapse')) {\n            this._toggleCollapseEl(collapseEl, false);\n        }\n    },\n    /**\n     * Called when a widget notifies a preview/change/reset.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onUserValueUpdate: async function (ev) {\n        ev.stopPropagation();\n        const widget = ev.data.widget;\n        const previewMode = ev.data.previewMode;\n\n        // First check if the updated widget or any of the widgets it triggers\n        // will require a reload or a confirmation choice by the user. If it is\n        // the case, warn the user and potentially ask if he agrees to save its\n        // current changes. If not, just do nothing.\n        let requiresReload = false;\n        if (!ev.data.previewMode && !ev.data.isSimulatedEvent) {\n            const linkedWidgets = this._requestUserValueWidgets(...ev.data.triggerWidgetsNames);\n            const widgets = [ev.data.widget].concat(linkedWidgets);\n\n            const warnMessage = await this._checkIfWidgetsUpdateNeedWarning(widgets);\n            if (warnMessage) {\n                const okWarning = await new Promise(resolve => {\n                    this.dialog.add(ConfirmationDialog, {\n                        body: warnMessage,\n                        confirm: () => resolve(true),\n                        cancel: () => resolve(false),\n                    });\n                });\n                if (!okWarning) {\n                    return;\n                }\n            }\n\n            requiresReload = !!await this._checkIfWidgetsUpdateNeedReload(widgets);\n        }\n\n        // Queue action so that we can later skip useless actions.\n        if (!this._actionQueues.get(widget)) {\n            this._actionQueues.set(widget, []);\n        }\n        const currentAction = {previewMode};\n        this._actionQueues.get(widget).push(currentAction);\n\n        // Ask a mutexed snippet update according to the widget value change\n        const shouldRecordUndo = (!previewMode && !ev.data.isSimulatedEvent);\n        if (shouldRecordUndo) {\n            this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n        }\n        const useLoaderOnOptionPanel = ev.target.el.dataset.loaderOnOptionPanel;\n        this.trigger_up('snippet_edition_request', {exec: async () => {\n            // If some previous snippet edition in the mutex removed the target from\n            // the DOM, the widget can be destroyed, in that case the edition request\n            // is now useless and can be discarded.\n            if (this.isDestroyed()) {\n                return;\n            }\n            // Filter actions that are counterbalanced by earlier/later actions\n            const actionQueue = this._actionQueues.get(widget).filter(({previewMode}, i, actions) => {\n                const prev = actions[i - 1];\n                const next = actions[i + 1];\n                if (previewMode === true && next && next.previewMode) {\n                    return false;\n                } else if (previewMode === 'reset' && prev && prev.previewMode) {\n                    return false;\n                }\n                return true;\n            });\n            // Skip action if it's been counterbalanced\n            if (!actionQueue.includes(currentAction)) {\n                this._actionQueues.set(widget, actionQueue);\n                return;\n            }\n            this._actionQueues.set(widget, actionQueue.filter(action => action !== currentAction));\n\n            if (ev.data.prepare) {\n                ev.data.prepare();\n            }\n\n            if (previewMode && (widget.$el.closest('[data-no-preview=\"true\"]').length)) {\n                // TODO the flag should be fetched through widget params somehow\n                return;\n            }\n\n            // Call widget option methods and update $target\n            await this._select(previewMode, widget);\n\n            // If it is not preview mode, the user selected the option for good\n            // (so record the action)\n            if (shouldRecordUndo) {\n                this.options.wysiwyg.odooEditor.historyStep();\n            }\n\n            if (previewMode || requiresReload) {\n                return;\n            }\n\n            await new Promise(resolve => setTimeout(() => {\n                // Will update the UI of the correct widgets for all options\n                // related to the same $target/editor\n                this.trigger_up('snippet_option_update', {\n                    onSuccess: () => resolve(),\n                });\n            // Set timeout needed so that the user event which triggered the\n            // option can bubble first.\n            }));\n        }, optionsLoader: useLoaderOnOptionPanel});\n\n        if (ev.data.isSimulatedEvent) {\n            // If the user value update was simulated through a trigger, we\n            // prevent triggering further widgets. This could be allowed at some\n            // point but does not work correctly in complex website cases (see\n            // customizeWebsite).\n            return;\n        }\n\n        // Check linked widgets: force their value and simulate a notification\n        // It is possible that we don't have the widget, we continue because a\n        // reload might be needed. For example, change template header without\n        // being on a website.page (e.g: /shop).\n        const linkedWidgets = this._requestUserValueWidgets(...ev.data.triggerWidgetsNames);\n        let i = 0;\n        const triggerWidgetsValues = ev.data.triggerWidgetsValues;\n        for (const linkedWidget of linkedWidgets) {\n            const widgetValue = triggerWidgetsValues[i];\n            if (widgetValue !== undefined) {\n                // FIXME right now only make this work supposing it is a\n                // colorpicker widget with big big hacks, this should be\n                // improved a lot\n                const normValue = this._normalizeWidgetValue(widgetValue);\n                if (previewMode === true) {\n                    linkedWidget._previewColor = normValue;\n                } else if (previewMode === false) {\n                    linkedWidget._previewColor = false;\n                    linkedWidget._value = normValue;\n                } else {\n                    linkedWidget._previewColor = false;\n                }\n            }\n\n            linkedWidget.notifyValueChange(previewMode, true);\n            i++;\n        }\n\n        if (requiresReload) {\n            this.trigger_up('request_save', {\n                reloadEditor: true,\n                optionSelector: this.data.selector,\n                url: this.data.reload,\n            });\n        }\n    },\n    /**\n     * @private\n     */\n    _onUserValueWidgetCritical() {\n        this.trigger_up('remove_snippet', {\n            $snippet: this.$target,\n        });\n    },\n});\nconst registry = {};\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\nregistry.sizing = SnippetOptionWidget.extend({\n    displayOverlayOptions: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        const self = this;\n        const def = this._super.apply(this, arguments);\n        let isMobile = weUtils.isMobileView(this.$target[0]);\n\n        this.$handles = this.$overlay.find('.o_handle');\n\n        let resizeValues = this._getSize();\n        this.$handles.on('mousedown', function (ev) {\n            const mousedownTime = ev.timeStamp;\n            ev.preventDefault();\n            isMobile = weUtils.isMobileView(self.$target[0]);\n\n            // First update size values as some element sizes may not have been\n            // initialized on option start (hidden slides, etc)\n            resizeValues = self._getSize();\n            const $handle = $(ev.currentTarget);\n\n            let compass = false;\n            let XY = false;\n            if ($handle.hasClass('n')) {\n                compass = 'n';\n                XY = 'Y';\n            } else if ($handle.hasClass('s')) {\n                compass = 's';\n                XY = 'Y';\n            } else if ($handle.hasClass('e')) {\n                compass = 'e';\n                XY = 'X';\n            } else if ($handle.hasClass('w')) {\n                compass = 'w';\n                XY = 'X';\n            } else if ($handle.hasClass('nw')) {\n                compass = 'nw';\n                XY = 'YX';\n            } else if ($handle.hasClass('ne')) {\n                compass = 'ne';\n                XY = 'YX';\n            } else if ($handle.hasClass('sw')) {\n                compass = 'sw';\n                XY = 'YX';\n            } else if ($handle.hasClass('se')) {\n                compass = 'se';\n                XY = 'YX';\n            }\n\n            // Don't call the normal resize methods if we are in a grid and\n            // vice-versa.\n            const isGrid = Object.keys(resizeValues).length === 4;\n            const isGridHandle = $handle[0].classList.contains('o_grid_handle');\n            if (isGrid && !isGridHandle || !isGrid && isGridHandle) {\n                return;\n            }\n\n            let resizeVal;\n            if (compass.length > 1) {\n                resizeVal = [resizeValues[compass[0]], resizeValues[compass[1]]];\n            } else {\n                resizeVal = [resizeValues[compass]];\n            }\n\n            if (resizeVal.some(rV => !rV)) {\n                return;\n            }\n\n            // Locking the mutex during the resize. Started here to avoid\n            // empty returns.\n            let resizeResolve;\n            const prom = new Promise(resolve => resizeResolve = () => resolve());\n            self.trigger_up(\"snippet_edition_request\", { exec: () => {\n                self.trigger_up(\"disable_loading_effect\");\n                return prom;\n            }});\n\n            // If we are in grid mode, add a background grid and place it in\n            // front of the other elements.\n            const rowEl = self.$target[0].parentNode;\n            let backgroundGridEl;\n            if (rowEl.classList.contains(\"o_grid_mode\") && !isMobile) {\n                self.options.wysiwyg.odooEditor.observerUnactive('displayBackgroundGrid');\n                backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, 0);\n                gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl);\n                self.options.wysiwyg.odooEditor.observerActive('displayBackgroundGrid');\n            }\n\n            // For loop to handle the cases where it is ne, nw, se or sw. Since\n            // there are two directions, we compute for both directions and we\n            // store the values in an array.\n            const directions = [];\n            for (const [i, resize] of resizeVal.entries()) {\n                const props = {};\n                let current = 0;\n                const cssProperty = resize[2];\n                const cssPropertyValue = parseInt(self.$target.css(cssProperty));\n                resize[0].forEach((val, key) => {\n                    if (self.$target.hasClass(val)) {\n                        current = key;\n                    } else if (resize[1][key] === cssPropertyValue) {\n                        current = key;\n                    }\n                });\n\n                props.resize = resize;\n                props.current = current;\n                props.begin = current;\n                props.beginClass = self.$target.attr('class');\n                props.regClass = new RegExp('\\\\s*' + resize[0][current].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');\n                props.xy = ev['page' + XY[i]];\n                props.XY = XY[i];\n                props.compass = compass[i];\n\n                directions.push(props);\n            }\n\n            self.options.wysiwyg.odooEditor.automaticStepUnactive('resizing');\n\n            const cursor = $handle.css('cursor') + '-important';\n            const $iframeWindow = $(this.ownerDocument.defaultView);\n            $iframeWindow[0].document.body.classList.add(cursor);\n            self.$overlay.removeClass('o_handlers_idle');\n\n            const iframeWindowMouseMove = function (ev) {\n                ev.preventDefault();\n\n                let changeTotal = false;\n                for (const dir of directions) {\n                    // dd is the number of pixels by which the mouse moved,\n                    // compared to the initial position of the handle.\n                    const dd = ev['page' + dir.XY] - dir.xy + dir.resize[1][dir.begin];\n                    const next = dir.current + (dir.current + 1 === dir.resize[1].length ? 0 : 1);\n                    const prev = dir.current ? (dir.current - 1) : 0;\n\n                    let change = false;\n                    // If the mouse moved to the right/down by at least 2/3 of\n                    // the space between the previous and the next steps, the\n                    // handle is snapped to the next step and the class is\n                    // replaced by the one matching this step.\n                    if (dd > (2 * dir.resize[1][next] + dir.resize[1][dir.current]) / 3) {\n                        self.$target.attr('class', (self.$target.attr('class') || '').replace(dir.regClass, ''));\n                        self.$target.addClass(dir.resize[0][next]);\n                        dir.current = next;\n                        change = true;\n                    }\n                    // Same as above but to the left/up.\n                    if (prev !== dir.current && dd < (2 * dir.resize[1][prev] + dir.resize[1][dir.current]) / 3) {\n                        self.$target.attr('class', (self.$target.attr('class') || '').replace(dir.regClass, ''));\n                        self.$target.addClass(dir.resize[0][prev]);\n                        dir.current = prev;\n                        change = true;\n                    }\n\n                    if (change) {\n                        self._onResize(dir.compass, dir.beginClass, dir.current);\n                    }\n\n                    changeTotal = changeTotal || change;\n                }\n\n                if (changeTotal) {\n                    self.trigger_up('cover_update');\n                    $handle.addClass('o_active');\n                }\n            };\n            const iframeWindowMouseUp = function (ev) {\n                $iframeWindow.off(\"mousemove\", iframeWindowMouseMove);\n                $iframeWindow.off(\"mouseup\", iframeWindowMouseUp);\n                $iframeWindow[0].document.body.classList.remove(cursor);\n                self.$overlay.addClass('o_handlers_idle');\n                $handle.removeClass('o_active');\n\n                // If we are in grid mode, removes the background grid.\n                // Also sync the col-* class with the g-col-* class so the\n                // toggle to normal mode and the mobile view are well done.\n                if (rowEl.classList.contains(\"o_grid_mode\") && !isMobile) {\n                    self.options.wysiwyg.odooEditor.observerUnactive('displayBackgroundGrid');\n                    backgroundGridEl.remove();\n                    self.options.wysiwyg.odooEditor.observerActive('displayBackgroundGrid');\n                    gridUtils._resizeGrid(rowEl);\n\n                    const colClass = [...self.$target[0].classList].find(c => /^col-/.test(c));\n                    const gColClass = [...self.$target[0].classList].find(c => /^g-col-/.test(c));\n                    self.$target[0].classList.remove(colClass);\n                    self.$target[0].classList.add(gColClass.substring(2));\n                }\n\n                self.options.wysiwyg.odooEditor.automaticStepActive('resizing');\n\n                // Freeing the mutex once the resizing is done.\n                resizeResolve();\n                self.trigger_up(\"enable_loading_effect\");\n\n                // Check whether there has been a resizing.\n                if (directions.every(dir => dir.begin === dir.current)) {\n                    const mouseupTime = ev.timeStamp;\n                    // Mouse held duration in milliseconds.\n                    const mouseHeldDuration = mouseupTime - mousedownTime;\n                    // If no resizing happened and if the mouse was pressed less\n                    // than 500 ms, we assume that the user wanted to click on\n                    // the element behind the handle.\n                    if (mouseHeldDuration < 500) {\n                        // Find the first element behind the overlay.\n                        const sameCoordinatesEls = self.ownerDocument\n                            .elementsFromPoint(ev.pageX, ev.pageY);\n                        // Check toBeClickEl has native JS `click` function\n                        const toBeClickedEl = sameCoordinatesEls\n                            .find(el => !el.closest(\"#oe_manipulators\") && typeof el.click === \"function\");\n                        if (toBeClickedEl) {\n                            toBeClickedEl.click();\n                        }\n                    }\n                    return;\n                }\n\n                setTimeout(function () {\n                    self.options.wysiwyg.odooEditor.historyStep();\n\n                    self.trigger_up(\"snippet_edition_request\", { exec: async () => {\n                        await new Promise(resolve => {\n                            self.trigger_up(\"snippet_option_update\", { onSuccess: () => resolve() });\n                        });\n                    }});\n                }, 0);\n            };\n            $iframeWindow.on(\"mousemove\", iframeWindowMouseMove);\n            $iframeWindow.on(\"mouseup\", iframeWindowMouseUp);\n        });\n\n        for (const [key, value] of Object.entries(resizeValues)) {\n            this.$handles.filter('.' + key).toggleClass('readonly', !value);\n        }\n        if (!isMobile && this.$target[0].classList.contains(\"o_grid_item\")) {\n            this.$handles.filter('.o_grid_handle').toggleClass('readonly', false);\n        }\n\n        return def;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUI() {\n        this._updateSizingHandles();\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    setTarget: function () {\n        this._super(...arguments);\n        // TODO master: _onResize should not be called here, need to check if\n        // updateUI is called when the target is changed\n        this._onResize();\n    },\n    /**\n     * @override\n     */\n    async updateUIVisibility() {\n        await this._super(...arguments);\n\n        const isMobileView = weUtils.isMobileView(this.$target[0]);\n        const isGridOn = this.$target[0].classList.contains(\"o_grid_item\");\n        const isGrid = !isMobileView && isGridOn;\n        if (this.$target[0].parentNode && this.$target[0].parentNode.classList.contains('row')) {\n            // Hiding/showing the correct resize handles if we are in grid mode\n            // or not.\n            for (const handleEl of this.$handles) {\n                const isGridHandle = handleEl.classList.contains('o_grid_handle');\n                handleEl.classList.toggle('d-none', isGrid ^ isGridHandle);\n                // Disabling the vertical resize if we are in mobile view.\n                const isVerticalSizing = handleEl.matches('.n, .s');\n                handleEl.classList.toggle(\"readonly\", isMobileView && isVerticalSizing && isGridOn);\n            }\n\n            // Hiding the move handle in mobile view so we can't drag the\n            // columns.\n            const moveHandleEl = this.$overlay[0].querySelector('.o_move_handle');\n            moveHandleEl.classList.toggle('d-none', isMobileView);\n\n            // Show/hide the buttons to send back/front a grid item.\n            const bringFrontBackEls = this.$overlay[0].querySelectorAll('.o_front_back');\n            bringFrontBackEls.forEach(button => button.classList.toggle(\"d-none\", !isGrid));\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns an object mapping one or several cardinal direction (n, e, s, w)\n     * to an Array containing:\n     * 1) A list of classes to toggle when using this cardinal direction\n     * 2) A list of values these classes are supposed to set on a given CSS prop\n     * 3) The mentioned CSS prop\n     *\n     * Note: this object must also be saved in this.grid before being returned.\n     *\n     * @abstract\n     * @private\n     * @returns {Object}\n     */\n    _getSize: function () {},\n    /**\n     * Called when the snippet is being resized and its classes changes.\n     *\n     * @private\n     * @param {string} [compass] - resize direction ('n', 's', 'e' or 'w')\n     * @param {string} [beginClass] - attributes class at the beginning\n     * @param {integer} [current] - current increment in this.grid\n     */\n    _onResize: function (compass, beginClass, current) {\n        this._updateSizingHandles();\n        this._notifyResizeChange();\n    },\n    /**\n     * @private\n     */\n    _updateSizingHandles: function () {\n        var self = this;\n\n        // Adapt the resize handles according to the classes and dimensions\n        var resizeValues = this._getSize();\n        var $handles = this.$overlay.find('.o_handle');\n        for (const [direction, resizeValue] of Object.entries(resizeValues)) {\n            var classes = resizeValue[0];\n            var values = resizeValue[1];\n            var cssProperty = resizeValue[2];\n\n            var $handle = $handles.filter('.' + direction);\n\n            var current = 0;\n            var cssPropertyValue = parseInt(self.$target.css(cssProperty));\n            classes.forEach((className, key) => {\n                if (self.$target.hasClass(className)) {\n                    current = key;\n                } else if (values[key] === cssPropertyValue) {\n                    current = key;\n                }\n            });\n\n            $handle.toggleClass('o_handle_start', current === 0);\n            $handle.toggleClass('o_handle_end', current === classes.length - 1);\n        }\n\n        // Adapt the handles to fit top and bottom sizes\n        this.$overlay.find('.o_handle:not(.o_grid_handle)').filter(\".n, .s\").toArray().forEach(handle => {\n            var $handle = $(handle);\n            var direction = $handle.hasClass('n') ? 'top' : 'bottom';\n            $handle.outerHeight(self.$target.css('padding-' + direction));\n        });\n    },\n    /**\n     * @override\n     */\n    async _notifyResizeChange() {\n        this.$target.trigger('content_changed');\n    },\n});\n\n/**\n * Handles the edition of padding-top and padding-bottom.\n */\nregistry['sizing_y'] = registry.sizing.extend({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getSize: function () {\n        var nClass = 'pt';\n        var nProp = 'padding-top';\n        var sClass = 'pb';\n        var sProp = 'padding-bottom';\n        if (this.$target.is('hr')) {\n            nClass = 'mt';\n            nProp = 'margin-top';\n            sClass = 'mb';\n            sProp = 'margin-bottom';\n        }\n\n        var grid = [];\n        for (var i = 0; i <= (256 / 8); i++) {\n            grid.push(i * 8);\n        }\n        grid.splice(1, 0, 4);\n        this.grid = {\n            n: [grid.map(v => nClass + v), grid, nProp],\n            s: [grid.map(v => sClass + v), grid, sProp],\n        };\n        return this.grid;\n    },\n});\nregistry['sizing_x'] = registry.sizing.extend({\n    /**\n     * @override\n     */\n    onClone: function (options) {\n        this._super.apply(this, arguments);\n        // Below condition is added to remove offset of target element only\n        // and not its children to avoid design alteration of a container/block.\n        if (options.isCurrent) {\n            const targetClassList = this.$target[0].classList;\n            const offsetClasses = [...targetClassList]\n                .filter(cls => cls.match(/^offset-(lg-)?([0-9]{1,2})$/));\n            targetClassList.remove(...offsetClasses);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getSize: function () {\n        const isMobileView = weUtils.isMobileView(this.$target[0]);\n        const resolutionModifier = isMobileView ? \"\" : \"lg-\";\n        var width = this.$target.closest('.row').width();\n        var gridE = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\n        var gridW = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];\n        this.grid = {\n            e: [\n                gridE.map(v => (`col-${resolutionModifier}${v}`)),\n                gridE.map(v => width / 12 * v),\n                \"width\",\n            ],\n            w: [\n                gridW.map(v => (`offset-${resolutionModifier}${v}`)),\n                gridW.map(v => width / 12 * v),\n                \"margin-left\",\n            ],\n        };\n        return this.grid;\n    },\n    /**\n     * @override\n     */\n    _onResize: function (compass, beginClass, current) {\n        const targetEl = this.$target[0];\n        const isMobileView = weUtils.isMobileView(targetEl);\n        const resolutionModifier = isMobileView ? \"\" : \"lg-\";\n\n        if (compass === 'w' || compass === 'e') {\n            // (?!\\S): following char cannot be a non-space character\n            const offsetRegex = new RegExp(`(?:^|\\\\s+)offset-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n            const colRegex = new RegExp(`(?:^|\\\\s+)col-${resolutionModifier}(\\\\d{1,2})(?!\\\\S)`);\n\n            const beginOffset = Number(beginClass.match(offsetRegex)?.[1] || 0);\n\n            if (compass === 'w') {\n                // don't change the right border position when we change the offset (replace col size)\n                const beginCol = Number(beginClass.match(colRegex)?.[1] || 12);\n                let offset = Number(this.grid.w[0][current].match(offsetRegex)?.[1] || 0);\n                if (offset < 0) {\n                    offset = 0;\n                }\n                let colSize = beginCol - (offset - beginOffset);\n                if (colSize <= 0) {\n                    colSize = 1;\n                    offset = beginOffset + beginCol - 1;\n                }\n                const offsetColRegex = new RegExp(`${offsetRegex.source}|${colRegex.source}`, \"g\");\n                targetEl.className = targetEl.className.replace(offsetColRegex, \"\");\n                targetEl.classList.add(`col-${resolutionModifier}${colSize > 12 ? 12 : colSize}`);\n\n                if (offset > 0) {\n                    targetEl.classList.add(`offset-${resolutionModifier}${offset}`);\n                }\n                if (isMobileView && offset === 0) {\n                    targetEl.classList.remove(\"offset-lg-0\");\n                } else if ((isMobileView && offset > 0 &&\n                        !targetEl.className.match(/(^|\\s+)offset-lg-\\d{1,2}(?!\\S)/)) ||\n                        (!isMobileView && offset === 0 &&\n                        targetEl.className.match(/(^|\\s+)offset-\\d{1,2}(?!\\S)/))) {\n                    targetEl.classList.add(\"offset-lg-0\");\n                }\n            } else if (beginOffset > 0) {\n                const endCol = Number(this.grid.e[0][current].match(colRegex)?.[1] || 0);\n                // Avoids overflowing the grid to the right if the\n                // column size + the offset exceeds 12.\n                if ((endCol + beginOffset) > 12) {\n                    targetEl.className = targetEl.className.replace(colRegex, \"\");\n                    targetEl.classList.add(`col-${resolutionModifier}${12 - beginOffset}`);\n                }\n            }\n        }\n        this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    async _notifyResizeChange() {\n        this.trigger_up('option_update', {\n            optionName: 'StepsConnector',\n            name: 'change_column_size',\n        });\n        this._super.apply(this, arguments);\n    },\n});\n\n/**\n * Handles the sizing in grid mode: edition of grid-{column|row}-{start|end}.\n */\nregistry['sizing_grid'] = registry.sizing.extend({\n    /**\n     * @override\n     */\n    _getSize() {\n        const rowEl = this.$target.closest('.row')[0];\n        const gridProp = gridUtils._getGridProperties(rowEl);\n\n        const rowStart = this.$target[0].style.gridRowStart;\n        const rowEnd = parseInt(this.$target[0].style.gridRowEnd);\n        const columnStart = this.$target[0].style.gridColumnStart;\n        const columnEnd = this.$target[0].style.gridColumnEnd;\n\n        const gridN = [];\n        const gridS = [];\n        for (let i = 1; i < rowEnd + 12; i++) {\n            gridN.push(i);\n            gridS.push(i + 1);\n        }\n        const gridW = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12];\n        const gridE = [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];\n\n        this.grid = {\n            n: [gridN.map(v => ('g-height-' + (rowEnd - v))), gridN.map(v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-start'],\n            s: [gridS.map(v => ('g-height-' + (v - rowStart))), gridS.map(v => ((gridProp.rowSize + gridProp.rowGap) * (v - 1))), 'grid-row-end'],\n            w: [gridW.map(v => ('g-col-lg-' + (columnEnd - v))), gridW.map(v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-start'],\n            e: [gridE.map(v => ('g-col-lg-' + (v - columnStart))), gridE.map(v => ((gridProp.columnSize + gridProp.columnGap) * (v - 1))), 'grid-column-end'],\n        };\n\n        return this.grid;\n    },\n    /**\n     * @override\n     */\n    _onResize(compass, beginClass, current) {\n        if (compass === 'n') {\n            const rowEnd = parseInt(this.$target[0].style.gridRowEnd);\n            if (current < 0) {\n                this.$target[0].style.gridRowStart = 1;\n            } else if (current + 1 >= rowEnd) {\n                this.$target[0].style.gridRowStart = rowEnd - 1;\n            } else {\n                this.$target[0].style.gridRowStart = current + 1;\n            }\n        } else if (compass === 's') {\n            const rowStart = parseInt(this.$target[0].style.gridRowStart);\n            const rowEnd = parseInt(this.$target[0].style.gridRowEnd);\n            if (current + 2 <= rowStart) {\n                this.$target[0].style.gridRowEnd = rowStart + 1;\n            } else {\n                this.$target[0].style.gridRowEnd = current + 2;\n            }\n\n            // Updating the grid height.\n            const rowEl = this.$target[0].parentNode;\n            const rowCount = parseInt(rowEl.dataset.rowCount);\n            const backgroundGridEl = rowEl.querySelector('.o_we_background_grid');\n            const backgroundGridRowEnd = parseInt(backgroundGridEl.style.gridRowEnd);\n            let rowMove = 0;\n            if (this.$target[0].style.gridRowEnd > rowEnd && this.$target[0].style.gridRowEnd > rowCount + 1) {\n                rowMove = this.$target[0].style.gridRowEnd - rowEnd;\n            } else if (this.$target[0].style.gridRowEnd < rowEnd && this.$target[0].style.gridRowEnd >= rowCount + 1) {\n                rowMove = this.$target[0].style.gridRowEnd - rowEnd;\n            }\n            backgroundGridEl.style.gridRowEnd = backgroundGridRowEnd + rowMove;\n        } else if (compass === 'w') {\n            const columnEnd = parseInt(this.$target[0].style.gridColumnEnd);\n            if (current < 0) {\n                this.$target[0].style.gridColumnStart = 1;\n            } else if (current + 1 >= columnEnd) {\n                this.$target[0].style.gridColumnStart = columnEnd - 1;\n            } else {\n                this.$target[0].style.gridColumnStart = current + 1;\n            }\n        } else if (compass === 'e') {\n            const columnStart = parseInt(this.$target[0].style.gridColumnStart);\n            if (current + 2 > 13) {\n                this.$target[0].style.gridColumnEnd = 13;\n            } else if (current + 2 <= columnStart) {\n                this.$target[0].style.gridColumnEnd = columnStart + 1;\n            } else {\n                this.$target[0].style.gridColumnEnd = current + 2;\n            }\n        }\n\n        if (compass === 'n' || compass === 's') {\n            const numberRows = this.$target[0].style.gridRowEnd - this.$target[0].style.gridRowStart;\n            this.$target.attr('class', this.$target.attr('class').replace(/\\s*(g-height-)([0-9-]+)/g, ''));\n            this.$target.addClass('g-height-' + numberRows);\n        }\n\n        if (compass === 'w' || compass === 'e') {\n            const numberColumns = this.$target[0].style.gridColumnEnd - this.$target[0].style.gridColumnStart;\n            this.$target.attr('class', this.$target.attr('class').replace(/\\s*(g-col-lg-)([0-9-]+)/g, ''));\n            this.$target.addClass('g-col-lg-' + numberColumns);\n        }\n    },\n});\n\n/**\n * Controls box properties.\n */\nregistry.Box = SnippetOptionWidget.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * TODO this should be reviewed in master to avoid the need of using the\n     * 'reset' previewMode and having to remember the previous box-shadow value.\n     * We are forced to remember the previous box shadow before applying a new\n     * one as the whole box-shadow value is handled by multiple widgets.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setShadow(previewMode, widgetValue, params) {\n        // Check if the currently configured shadow is not using the same shadow\n        // mode, in which case nothing has to be done.\n        const styles = window.getComputedStyle(this.$target[0]);\n        const currentBoxShadow = styles['box-shadow'] || 'none';\n        const currentMode = currentBoxShadow === 'none'\n            ? ''\n            : currentBoxShadow.includes('inset') ? 'inset' : 'outset';\n        if (currentMode === widgetValue) {\n            return;\n        }\n\n        if (previewMode === true) {\n            this._prevBoxShadow = currentBoxShadow;\n        }\n\n        // Add/remove the shadow class\n        this.$target.toggleClass(params.shadowClass, !!widgetValue);\n\n        // Change the mode of the old box shadow. If no shadow was currently\n        // set then get the shadow value that is supposed to be set according\n        // to the shadow mode. Try to apply it via the selectStyle method so\n        // that it is either ignored because the shadow class had its effect or\n        // forced (to the shadow value or none) if toggling the class is not\n        // enough (e.g. if the item has a default shadow coming from CSS rules,\n        // removing the shadow class won't be enough to remove the shadow but in\n        // most other cases it will).\n        let shadow = 'none';\n        if (previewMode === 'reset') {\n            shadow = this._prevBoxShadow;\n        } else {\n            if (currentBoxShadow === 'none') {\n                shadow = this._getDefaultShadow(widgetValue, params.shadowClass);\n            } else {\n                if (widgetValue === 'outset') {\n                    shadow = currentBoxShadow.replace('inset', '').trim();\n                } else if (widgetValue === 'inset') {\n                    shadow = currentBoxShadow + ' inset';\n                }\n            }\n        }\n        await this.selectStyle(previewMode, shadow, Object.assign({cssProperty: 'box-shadow'}, params));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'setShadow') {\n            const shadowValue = this.$target.css('box-shadow');\n            if (!shadowValue || shadowValue === 'none') {\n                return '';\n            }\n            return this.$target.css('box-shadow').includes('inset') ? 'inset' : 'outset';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'fake_inset_shadow_opt') {\n            return false;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     * @param {string} type\n     * @param {string} shadowClass\n     * @returns {string}\n     */\n    _getDefaultShadow(type, shadowClass) {\n        if (!type) {\n            return 'none';\n        }\n\n        const el = document.createElement('div');\n        el.classList.add(shadowClass);\n        document.body.appendChild(el);\n        const shadow = `${$(el).css('box-shadow')}${type === 'inset' ? ' inset' : ''}`;\n        el.remove();\n        return shadow;\n    },\n});\n\n\n\nregistry.layout_column = SnippetOptionWidget.extend(ColumnLayoutMixin, {\n    /**\n     * @override\n     */\n    cleanUI() {\n        this._removeGridPreview();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Changes the number of columns.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectCount: async function (previewMode, widgetValue, params) {\n        // Make sure the \"Custom\" option is read-only.\n        if (widgetValue === \"custom\") {\n            return;\n        }\n        const previousNbColumns = this.$('> .row').children().length;\n        let $row = this.$('> .row');\n        if (!$row.length) {\n            const restoreCursor = preserveCursor(this.$target[0].ownerDocument);\n            resetOuids(this.$target[0]);\n            $row = this.$target.contents().wrapAll($('<div class=\"row\"><div class=\"col-lg-12\"/></div>')).parent().parent();\n            restoreCursor();\n        }\n\n        const nbColumns = parseInt(widgetValue);\n        await this._updateColumnCount($row[0], (nbColumns || 1));\n        // Yield UI thread to wait for event to bubble before activate_snippet is called.\n        // In this case this lets the select handle the click event before we switch snippet.\n        // TODO: make this more generic in activate_snippet event handler.\n        await new Promise(resolve => setTimeout(resolve));\n        if (nbColumns === 0) {\n            const restoreCursor = preserveCursor(this.$target[0].ownerDocument);\n            resetOuids($row[0]);\n            $row.contents().unwrap().contents().unwrap();\n            restoreCursor();\n            this.trigger_up('activate_snippet', {$snippet: this.$target});\n        } else if (previousNbColumns === 0) {\n            this.trigger_up('activate_snippet', {$snippet: this.$('> .row').children().first()});\n        }\n        this.trigger_up('option_update', {\n            optionName: 'StepsConnector',\n            name: 'change_columns',\n        });\n    },\n    /**\n     * Changes the layout (columns or grid).\n     *\n     * @see this.selectClass for parameters\n     */\n    async selectLayout(previewMode, widgetValue, params) {\n        if (widgetValue === \"grid\") {\n            const rowEl = this.$target[0].querySelector('.row');\n            if (!rowEl || !rowEl.classList.contains('o_grid_mode')) { // Prevent toggling grid mode twice.\n                gridUtils._toggleGridMode(this.$target[0]);\n                this.trigger_up('activate_snippet', {$snippet: this.$target});\n            }\n        } else {\n            // Toggle normal mode only if grid mode was activated (as it's in\n            // normal mode by default).\n            const rowEl = this.$target[0].querySelector('.row');\n            if (rowEl && rowEl.classList.contains('o_grid_mode')) {\n                this._toggleNormalMode(rowEl);\n                this.trigger_up('activate_snippet', {$snippet: this.$target});\n            }\n        }\n        this.trigger_up('option_update', {\n            optionName: 'StepsConnector',\n            name: 'change_columns',\n        });\n    },\n    /**\n     * Adds an image, some text or a button in the grid.\n     *\n     * @see this.selectClass for parameters\n     */\n    async addElement(previewMode, widgetValue, params) {\n        const rowEl = this.$target[0].querySelector('.row');\n        const elementType = widgetValue;\n\n        // If it has been less than 15 seconds that we have added an element,\n        // shift the new element right and down by one cell. Otherwise, put it\n        // on the top left corner.\n        const currentTime = new Date().getTime();\n        if (this.lastAddTime && (currentTime - this.lastAddTime) / 1000 < 15) {\n            this.lastStartPosition = [this.lastStartPosition[0] + 1, this.lastStartPosition[1] + 1];\n        } else {\n            this.lastStartPosition = [1, 1]; // [rowStart, columnStart]\n        }\n        this.lastAddTime = currentTime;\n\n        // Create the new column.\n        const newColumnEl = document.createElement('div');\n        newColumnEl.classList.add('o_grid_item');\n        let numberColumns, numberRows;\n        let imageLoadedPromise;\n\n        if (elementType === 'image') {\n            // Set the columns properties.\n            newColumnEl.classList.add('col-lg-6', 'g-col-lg-6', 'g-height-6', 'o_grid_item_image');\n            numberColumns = 6;\n            numberRows = 6;\n\n            // Choose an image with the media dialog.\n            let isImageSaved = false;\n            await new Promise(resolve => {\n                this.call(\"dialog\", \"add\", MediaDialog, {\n                    onlyImages: true,\n                    save: imageEl => {\n                        isImageSaved = true;\n                        imageLoadedPromise = new Promise(resolve => {\n                            imageEl.addEventListener(\"load\", () => resolve(), {once: true});\n                        });\n                        // Adds the image to the new column.\n                        newColumnEl.appendChild(imageEl);\n                    },\n                }, {\n                    onClose: () => resolve()\n                });\n            });\n            if (!isImageSaved) {\n                // Revert the current step to exclude the step saved when the\n                // media dialog closed.\n                this.options.wysiwyg.odooEditor.historyRevertCurrentStep();\n                return;\n            }\n        } else if (elementType === 'text') {\n            newColumnEl.classList.add('col-lg-4', 'g-col-lg-4', 'g-height-2');\n            numberColumns = 4;\n            numberRows = 2;\n\n            // Create default text content.\n            const pEl = document.createElement('p');\n            pEl.classList.add('o_default_snippet_text');\n            pEl.textContent = _t(\"Write something...\");\n\n            newColumnEl.appendChild(pEl);\n        } else if (elementType === 'button') {\n            newColumnEl.classList.add('col-lg-2', 'g-col-lg-2', 'g-height-1');\n            numberColumns = 2;\n            numberRows = 1;\n\n            // Create default button.\n            const aEl = document.createElement('a');\n            aEl.href = '#';\n            aEl.classList.add('mb-2', 'btn', 'btn-primary');\n            aEl.textContent = \"Button\";\n\n            newColumnEl.appendChild(aEl);\n        }\n        // Place the column in the grid.\n        const rowStart = this.lastStartPosition[0];\n        let columnStart = this.lastStartPosition[1];\n        if (columnStart + numberColumns > 13) {\n            columnStart = 1;\n            this.lastStartPosition[1] = columnStart;\n        }\n        newColumnEl.style.gridArea = `${rowStart} / ${columnStart} / ${rowStart + numberRows} / ${columnStart + numberColumns}`;\n\n        // Setting the z-index to the maximum of the grid.\n        gridUtils._setElementToMaxZindex(newColumnEl, rowEl);\n\n        // Add the new column and update the grid height.\n        rowEl.appendChild(newColumnEl);\n        gridUtils._resizeGrid(rowEl);\n\n        // Scroll to the new column if more than half of it is hidden (= out of\n        // the viewport or hidden by an other element).\n        if (elementType === \"image\") {\n            // If an image was added, wait for it to be loaded before scrolling.\n            await imageLoadedPromise;\n        }\n        const newColumnPosition = newColumnEl.getBoundingClientRect();\n        const middleX = (newColumnPosition.left + newColumnPosition.right) / 2;\n        const middleY = (newColumnPosition.top + newColumnPosition.bottom) / 2;\n        const sameCoordinatesEl = this.ownerDocument.elementFromPoint(middleX, middleY);\n        if (!sameCoordinatesEl || !newColumnEl.contains(sameCoordinatesEl)) {\n            newColumnEl.scrollIntoView({behavior: \"smooth\", block: \"center\"});\n        }\n        this.trigger_up('activate_snippet', {$snippet: $(newColumnEl)});\n    },\n    /**\n     * @override\n     */\n    async selectStyle(previewMode, widgetValue, params) {\n        await this._super(previewMode, widgetValue, params);\n\n        const rowEl = this.$target[0];\n        const isMobileView = weUtils.isMobileView(rowEl);\n        if ([\"row-gap\", \"column-gap\"].includes(params.cssProperty) && !isMobileView) {\n            // Reset the animation.\n            this._removeGridPreview();\n            void rowEl.offsetWidth; // Trigger a DOM reflow.\n\n            // Add an animated grid preview.\n            this.options.wysiwyg.odooEditor.observerUnactive(\"addGridPreview\");\n            this.gridPreviewEl = gridUtils._addBackgroundGrid(rowEl, 0);\n            this.gridPreviewEl.classList.add(\"o_we_grid_preview\");\n            gridUtils._setElementToMaxZindex(this.gridPreviewEl, rowEl);\n            this.options.wysiwyg.odooEditor.observerActive(\"addGridPreview\");\n            this.removeGridPreview = this._removeGridPreview.bind(this);\n            rowEl.addEventListener(\"animationend\", this.removeGridPreview);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'selectCount') {\n            const isMobile = this._isMobile();\n            const columnEls = this.$target[0].querySelector(\":scope > .row\")?.children;\n            return this._getNbColumns(columnEls, isMobile);\n        } else if (methodName === 'selectLayout') {\n            const rowEl = this.$target[0].querySelector('.row');\n            if (rowEl && rowEl.classList.contains('o_grid_mode')) {\n                return \"grid\";\n            } else {\n                return 'normal';\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'zero_cols_opt') {\n            // Note: \"s_allow_columns\" indicates containers which may have\n            // bare content (without columns) and are allowed to have columns.\n            // By extension, we only show the \"None\" option on elements that\n            // were marked as such as they were allowed to have bare content in\n            // the first place.\n            return this.$target.is('.s_allow_columns');\n        } else if (widgetName === \"column_count_opt\") {\n            // Hide the selectCount widget if the `s_nb_column_fixed` class is\n            // on the row.\n            return !this.$target[0].querySelector(\":scope > .row.s_nb_column_fixed\");\n        } else if (widgetName === \"custom_cols_opt\") {\n            // Show \"Custom\" if the user altered the columns in some way (i.e.\n            // by adding offsets or resizing a column). This is only shown as\n            // an indication, but shouldn't be selectable.\n            const isMobile = this._isMobile();\n            return this.$target[0].querySelector(\":scope > .row\") &&\n                this._areColsCustomized(this.$target[0].querySelector(\":scope > .row\").children,\n                isMobile);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * If the number of columns requested is greater than the number of items,\n     * adds new columns which are clones of the last one. If there are less\n     * columns than the number of items, reorganizes the elements on the right\n     * amount of rows.\n     *\n     * @private\n     * @param {HTMLElement} rowEl - the row in which to update the columns\n     * @param {integer} nbColumns - the number of columns requested\n     */\n    async _updateColumnCount(rowEl, nbColumns) {\n        const isMobile = this._isMobile();\n        // The number of elements per row before the update.\n        const prevNbColumns = this._getNbColumns(rowEl.children, isMobile);\n\n        if (nbColumns === prevNbColumns) {\n            return;\n        }\n        this._resizeColumns(rowEl.children, nbColumns);\n\n        const itemsDelta = nbColumns - rowEl.children.length;\n        if (itemsDelta > 0) {\n            const newItems = [];\n            for (let i = 0; i < itemsDelta; i++) {\n                const lastEl = rowEl.lastElementChild;\n                newItems.push(new Promise(resolve => {\n                    this.trigger_up(\"clone_snippet\", {$snippet: $(lastEl), onSuccess: resolve});\n                }));\n            }\n            await Promise.all(newItems);\n        }\n\n        this.trigger_up('cover_update');\n    },\n    /**\n     * Resizes the columns for the mobile or desktop view.\n     *\n     * @private\n     * @param {HTMLCollection} columnEls - the elements to resize\n     * @param {integer} nbColumns - the number of wanted columns\n     */\n    _resizeColumns(columnEls, nbColumns) {\n        const isMobile = this._isMobile();\n        const itemSize = Math.floor(12 / nbColumns) || 1;\n        const firstItem = this._getFirstItem(columnEls, isMobile);\n        const firstItemOffset = Math.floor((12 - itemSize * nbColumns) / 2);\n\n        const resolutionModifier = isMobile ? \"\" : \"-lg\";\n        const replacingRegex =\n            // (?!\\S): following char cannot be a non-space character\n            new RegExp(`(?:^|\\\\s+)(col|offset)${resolutionModifier}(-\\\\d{1,2})?(?!\\\\S)`, \"g\");\n\n        for (const columnEl of columnEls) {\n            columnEl.className = columnEl.className.replace(replacingRegex, \"\");\n            columnEl.classList.add(`col${resolutionModifier}-${itemSize}`);\n\n            if (firstItemOffset && columnEl === firstItem) {\n                columnEl.classList.add(`offset${resolutionModifier}-${firstItemOffset}`);\n            }\n            const hasMobileOffset = columnEl.className.match(/(^|\\s+)offset-\\d{1,2}(?!\\S)/);\n            const hasDesktopOffset = columnEl.className.match(/(^|\\s+)offset-lg-[1-9][0-1]?(?!\\S)/);\n            columnEl.classList.toggle(\"offset-lg-0\", hasMobileOffset && !hasDesktopOffset);\n        }\n    },\n    /**\n     * Toggles the normal mode.\n     *\n     * @private\n     * @param {Element} rowEl\n     */\n    async _toggleNormalMode(rowEl) {\n        // Removing the grid class\n        rowEl.classList.remove('o_grid_mode');\n        const columnEls = rowEl.children;\n        // Removing the grid previews (if any).\n        await new Promise(resolve => {\n            this.trigger_up(\"clean_ui_request\", {\n                targetEl: this.$target[0].closest(\"section\"),\n                onSuccess: resolve,\n            });\n        });\n\n        for (const columnEl of columnEls) {\n            // Reloading the images.\n            gridUtils._reloadLazyImages(columnEl);\n            // Removing the grid properties.\n            gridUtils._convertToNormalColumn(columnEl);\n        }\n        // Removing the grid properties.\n        delete rowEl.dataset.rowCount;\n        // Kept for compatibility.\n        rowEl.style.removeProperty('--grid-item-padding-x');\n        rowEl.style.removeProperty('--grid-item-padding-y');\n        rowEl.style.removeProperty(\"gap\");\n    },\n    /**\n     * Removes the grid preview that was added when changing the grid gaps.\n     *\n     * @private\n     */\n    _removeGridPreview() {\n        this.options.wysiwyg.odooEditor.observerUnactive(\"removeGridPreview\");\n        this.$target[0].removeEventListener(\"animationend\", this.removeGridPreview);\n        if (this.gridPreviewEl) {\n            this.gridPreviewEl.remove();\n            delete this.gridPreviewEl;\n        }\n        delete this.removeGridPreview;\n        this.options.wysiwyg.odooEditor.observerActive(\"removeGridPreview\");\n    },\n    /**\n     * @returns {boolean}\n     */\n    _isMobile() {\n        return weUtils.isMobileView(this.$target[0]);\n    },\n});\n\nregistry.GridColumns = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    cleanUI() {\n        // Remove the padding highlights.\n        this._removePaddingPreview();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async selectStyle(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if ([\"--grid-item-padding-y\", \"--grid-item-padding-x\"].includes(params.cssProperty)) {\n            // Reset the animation.\n            this._removePaddingPreview();\n            void this.$target[0].offsetWidth; // Trigger a DOM reflow.\n\n            // Highlight the padding when changing it, by adding a pseudo-\n            // element with an animated colored border inside the grid item.\n            this.options.wysiwyg.odooEditor.observerUnactive(\"addPaddingPreview\");\n            this.$target[0].classList.add(\"o_we_padding_highlight\");\n            this.options.wysiwyg.odooEditor.observerActive(\"addPaddingPreview\");\n            this.removePaddingPreview = this._removePaddingPreview.bind(this);\n            this.$target[0].addEventListener(\"animationend\", this.removePaddingPreview);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if ([\"grid_padding_y_opt\", \"grid_padding_x_opt\"].includes(widgetName)) {\n            return this.$target[0].parentElement.classList.contains(\"o_grid_mode\");\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Removes the padding highlights that were added when changing the grid\n     * item padding.\n     *\n     * @private\n     */\n    _removePaddingPreview() {\n        this.options.wysiwyg.odooEditor.observerUnactive(\"removePaddingPreview\");\n        this.$target[0].removeEventListener(\"animationend\", this.removePaddingPreview);\n        this.$target[0].classList.remove(\"o_we_padding_highlight\");\n        delete this.removePaddingPreview;\n        this.options.wysiwyg.odooEditor.observerActive(\"removePaddingPreview\");\n    },\n});\n\nregistry.vAlignment = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        const value = await this._super(...arguments);\n        if (methodName === 'selectClass' && !value) {\n            // If there is no `align-items-` class on the row, then the `align-\n            // items-stretch` class is selected, because the behaviors are\n            // equivalent in both situations.\n            return 'align-items-stretch';\n        }\n        return value;\n    },\n});\n\n/**\n * Allows snippets to be moved before the preceding element or after the following.\n */\nregistry.SnippetMove = SnippetOptionWidget.extend(ColumnLayoutMixin, {\n    displayOverlayOptions: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        var $buttons = this.$el.find('we-button');\n        var $overlayArea = this.$overlay.find('.o_overlay_move_options');\n        // Putting the arrows side by side.\n        $overlayArea.prepend($buttons[1]);\n        $overlayArea.prepend($buttons[0]);\n\n        // Needed for compatibility (with already dropped snippets).\n        // If the target is a column, check if all the columns are either mobile\n        // ordered or not. If they are not consistent, then we remove the mobile\n        // order classes from all of them, to avoid issues.\n        const parentEl = this.$target[0].parentElement;\n        if (parentEl.classList.contains(\"row\")) {\n            const columnEls = [...parentEl.children];\n            const orderedColumnEls = columnEls.filter(el => el.style.order);\n            if (orderedColumnEls.length && orderedColumnEls.length !== columnEls.length) {\n                this._removeMobileOrders(orderedColumnEls);\n            }\n        }\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onClone(options) {\n        this._super.apply(this, arguments);\n        const mobileOrder = this.$target[0].style.order;\n        // If the order has been adapted on mobile, it must be different\n        // for each clone.\n        if (options.isCurrent && mobileOrder) {\n            const siblingEls = this.$target[0].parentElement.children;\n            const cloneEls = [...siblingEls].filter(el => el.style.order === mobileOrder);\n            // For cases in which multiple clones are made at the same time, we\n            // change the order for all clones at once. (e.g.: it happens when\n            // increasing the columns count.) This makes sure the clones get a\n            // mobile order in line with their DOM order.\n            cloneEls.forEach((el, i) => {\n                if (i > 0) {\n                    el.style.order = siblingEls.length - cloneEls.length + i;\n                }\n            });\n        }\n    },\n    /**\n     * @override\n     */\n    onMove() {\n        this._super.apply(this, arguments);\n        // Remove all the mobile order classes after a drag and drop.\n        this._removeMobileOrders(this.$target[0].parentElement.children);\n    },\n    /**\n     * @override\n     */\n    onRemove() {\n        this._super.apply(this, arguments);\n        const targetMobileOrder = this.$target[0].style.order;\n        // If the order has been adapted on mobile, the gap created by the\n        // removed snippet must be filled in.\n        if (targetMobileOrder) {\n            const targetOrder = parseInt(targetMobileOrder);\n            this._fillRemovedItemGap(this.$target[0].parentElement, targetOrder);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Moves the snippet around.\n     *\n     * @see this.selectClass for parameters\n     */\n    moveSnippet: function (previewMode, widgetValue, params) {\n        const isMobile = this._isMobile();\n        const isNavItem = this.$target[0].classList.contains('nav-item');\n        const $tabPane = isNavItem ? $(this.$target.find('.nav-link')[0].hash) : null;\n        const moveLeftOrRight = [\"move_left_opt\", \"move_right_opt\"].includes(params.name);\n\n        let siblingEls, mobileOrder;\n        if (moveLeftOrRight) {\n            siblingEls = this.$target[0].parentElement.children;\n            mobileOrder = !!this.$target[0].style.order;\n        }\n        if (moveLeftOrRight && isMobile && !isNavItem) {\n            if (!mobileOrder) {\n                this._addMobileOrders(siblingEls);\n            }\n            this._swapMobileOrders(widgetValue, siblingEls);\n        } else {\n            switch (widgetValue) {\n                case \"prev\": {\n                    // Consider only visible elements.\n                    let prevEl = this.$target[0].previousElementSibling;\n                    while (prevEl && window.getComputedStyle(prevEl).display === \"none\") {\n                        prevEl = prevEl.previousElementSibling;\n                    }\n                    prevEl?.insertAdjacentElement(\"beforebegin\", this.$target[0]);\n                    if (isNavItem) {\n                        $tabPane.prev().before($tabPane);\n                    }\n                    break;\n                }\n                case \"next\": {\n                    // Consider only visible elements.\n                    let nextEl = this.$target[0].nextElementSibling;\n                    while (nextEl && window.getComputedStyle(nextEl).display === \"none\") {\n                        nextEl = nextEl.nextElementSibling;\n                    }\n                    nextEl?.insertAdjacentElement(\"afterend\", this.$target[0]);\n                    if (isNavItem) {\n                        $tabPane.next().after($tabPane);\n                    }\n                    break;\n                }\n            }\n            if (mobileOrder) {\n                this._removeMobileOrders(siblingEls);\n            }\n        }\n        if (!this.$target.is(this.data.noScroll)\n                && (params.name === 'move_up_opt' || params.name === 'move_down_opt')) {\n            const mainScrollingEl = $().getScrollingElement()[0];\n            const elTop = this.$target[0].getBoundingClientRect().top;\n            const heightDiff = mainScrollingEl.offsetHeight - this.$target[0].offsetHeight;\n            const bottomHidden = heightDiff < elTop;\n            const hidden = elTop < 0 || bottomHidden;\n            if (hidden) {\n                scrollTo(this.$target[0], {\n                    extraOffset: 50,\n                    forcedOffset: bottomHidden ? heightDiff - 50 : undefined,\n                    duration: 500,\n                });\n            }\n        }\n        this.trigger_up('option_update', {\n            optionName: 'StepsConnector',\n            name: 'move_snippet',\n        });\n        // Update the \"Invisible Elements\" panel as the order of invisible\n        // snippets could have changed on the page.\n        this.trigger_up(\"update_invisible_dom\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        const moveUpOrLeft = widgetName === \"move_up_opt\" || widgetName === \"move_left_opt\";\n        const moveDownOrRight = widgetName === \"move_down_opt\" || widgetName === \"move_right_opt\";\n        const moveLeftOrRight = widgetName === \"move_left_opt\" || widgetName === \"move_right_opt\";\n\n        if (moveUpOrLeft || moveDownOrRight) {\n            // The arrows are not displayed if the target is in a grid and if\n            // not in mobile view.\n            const isMobileView = weUtils.isMobileView(this.$target[0]);\n            if (!isMobileView && this.$target[0].classList.contains(\"o_grid_item\")) {\n                return false;\n            }\n            // On mobile, items' reordering is independent from desktop inside\n            // a snippet (left or right), not at a higher level (up or down).\n            if (moveLeftOrRight && isMobileView) {\n                const targetMobileOrder = this.$target[0].style.order;\n                if (targetMobileOrder) {\n                    const siblingEls = this.$target[0].parentElement.children;\n                    const orderModifier = widgetName === \"move_left_opt\" ? -1 : 1;\n                    let delta = 0;\n                    while (true) {\n                        delta += orderModifier;\n                        const nextOrder = parseInt(targetMobileOrder) + delta;\n                        const siblingEl = [...siblingEls].find(el => el.style.order === nextOrder.toString());\n                        if (!siblingEl) {\n                            break;\n                        }\n                        if (window.getComputedStyle(siblingEl).display === \"none\") {\n                            continue;\n                        }\n                        return true;\n                    }\n                    return false;\n                }\n            }\n            // Consider only visible elements.\n            const direction = moveUpOrLeft ? \"previousElementSibling\" : \"nextElementSibling\";\n            let siblingEl = this.$target[0][direction];\n            while (siblingEl && window.getComputedStyle(siblingEl).display === \"none\") {\n                siblingEl = siblingEl[direction];\n            }\n            return !!siblingEl;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Swaps the mobile orders.\n     *\n     * @param {string} widgetValue\n     * @param {HTMLCollection} siblingEls\n     */\n    _swapMobileOrders(widgetValue, siblingEls) {\n        const targetMobileOrder = this.$target[0].style.order;\n        const orderModifier = widgetValue === \"prev\" ? -1 : 1;\n        let delta = 0;\n        while (true) {\n            delta += orderModifier;\n            const newOrder = parseInt(targetMobileOrder) + delta;\n            const comparedEl = [...siblingEls].find(el => el.style.order === newOrder.toString());\n            if (window.getComputedStyle(comparedEl).display === \"none\") {\n                continue;\n            }\n            this.$target[0].style.order = newOrder;\n            comparedEl.style.order = targetMobileOrder;\n            break;\n        }\n    },\n    /**\n     * @returns {Boolean}\n     */\n    _isMobile() {\n        return false;\n    },\n});\n\n/**\n * Allows for media to be replaced.\n */\nregistry.ReplaceMedia = SnippetOptionWidget.extend({\n    init: function () {\n        this._super(...arguments);\n        this._activateLinkTool = this._activateLinkTool.bind(this);\n        this._deactivateLinkTool = this._deactivateLinkTool.bind(this);\n    },\n\n    destroy: function () {\n        this._clearListeners();\n        return this._super(...arguments);\n    },\n\n    /**\n     * @override\n     */\n    onFocus() {\n        this.options.wysiwyg.odooEditor.addEventListener('activate_image_link_tool', this._activateLinkTool);\n        this.options.wysiwyg.odooEditor.addEventListener('deactivate_image_link_tool', this._deactivateLinkTool);\n        // When we start editing an image, rerender the UI to ensure the\n        // we-select that suggests the anchors is in a consistent state.\n        this.rerender = true;\n    },\n    /**\n     * @override\n     */\n    onBlur() {\n        this._clearListeners();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Replaces the media.\n     *\n     * @see this.selectClass for parameters\n     */\n    async replaceMedia() {\n        // open mediaDialog and replace the media.\n        await this.options.wysiwyg.openMediaDialog({ node:this.$target[0] });\n    },\n    /**\n     * Makes the image a clickable link by wrapping it in an <a>.\n     * This function is also called for the opposite operation.\n     *\n     * @see this.selectClass for parameters\n     */\n    setLink(previewMode, widgetValue, params) {\n        const parentEl = this._searchSupportedParentLinkEl();\n        if (parentEl.tagName !== 'A') {\n            const wrapperEl = document.createElement('a');\n            this.$target[0].after(wrapperEl);\n            wrapperEl.appendChild(this.$target[0]);\n            // TODO Remove when bug fixed in Chrome.\n            if (this.$target[0].getBoundingClientRect().width === 0) {\n                // Chrome lost lazy-loaded image => Force Chrome to display image.\n                const src = this.$target[0].src;\n                this.$target[0].src = '';\n                this.$target[0].src = src;\n            }\n        } else {\n            const fragment = document.createDocumentFragment();\n            fragment.append(...parentEl.childNodes);\n            parentEl.replaceWith(fragment);\n        }\n    },\n    /**\n     * Changes the image link so that the URL is opened on another tab or not\n     * when it is clicked.\n     *\n     * @see this.selectClass for parameters\n     */\n    setNewWindow(previewMode, widgetValue, params) {\n        const linkEl = this._searchSupportedParentLinkEl();\n        if (widgetValue) {\n            linkEl.setAttribute('target', '_blank');\n        } else {\n            linkEl.removeAttribute('target');\n        }\n    },\n    /**\n     * Records the target url of the hyperlink.\n     *\n     * @see this.selectClass for parameters\n     */\n    setUrl(previewMode, widgetValue, params) {\n        const linkEl = this._searchSupportedParentLinkEl();\n        let url = widgetValue;\n        if (!url) {\n            // As long as there is no URL, the image is not considered a link.\n            linkEl.removeAttribute('href');\n            this.$target.trigger('href_changed');\n            return;\n        }\n        if (!url.startsWith('/') && !url.startsWith('#')\n                && !/^([a-zA-Z]*.):.+$/gm.test(url)) {\n            // We permit every protocol (http:, https:, ftp:, mailto:,...).\n            // If none is explicitly specified, we assume it is a http.\n            url = 'http://' + url;\n        }\n        linkEl.setAttribute('href', url);\n        this.rerender = true;\n        this.$target.trigger('href_changed');\n    },\n    /**\n     * @override\n     */\n    async updateUI() {\n        if (this.rerender) {\n            this.rerender = false;\n            await this._rerenderXML();\n            return;\n        }\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _activateLinkTool() {\n        const parentEl = this._searchSupportedParentLinkEl();\n        if (parentEl.tagName === 'A') {\n            this._requestUserValueWidgets('media_url_opt')[0].focus();\n        } else {\n            this._requestUserValueWidgets('media_link_opt')[0].enable();\n        }\n    },\n    /**\n     * @private\n     */\n    _clearListeners() {\n        this.options.wysiwyg.odooEditor.removeEventListener('activate_image_link_tool', this._activateLinkTool);\n        this.options.wysiwyg.odooEditor.removeEventListener('deactivate_image_link_tool', this._deactivateLinkTool);\n    },\n    /**\n     * @private\n     */\n    _deactivateLinkTool() {\n        const parentEl = this._searchSupportedParentLinkEl();\n        if (parentEl.tagName === 'A') {\n            this._requestUserValueWidgets('media_link_opt')[0].enable();\n        }\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        const parentEl = this._searchSupportedParentLinkEl();\n        const linkEl = parentEl.tagName === 'A' ? parentEl : null;\n        switch (methodName) {\n            case 'setLink': {\n                return linkEl ? 'true' : '';\n            }\n            case 'setUrl': {\n                let href = linkEl ? linkEl.getAttribute('href') : '';\n                return href || '';\n            }\n            case 'setNewWindow': {\n                const target = linkEl ? linkEl.getAttribute('target') : '';\n                return target && target === '_blank' ? 'true' : '';\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'media_link_opt') {\n            if (this.$target[0].matches('img')) {\n                return isImageSupportedForStyle(this.$target[0])\n                    && !this._searchSupportedParentLinkEl().matches(\"a[data-oe-xpath]\");\n            }\n            return !this.$target[0].classList.contains('media_iframe_video');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     * @returns {Element} The \"closest\" element that can be supported as a <a>.\n     */\n    _searchSupportedParentLinkEl() {\n        const parentEl = this.$target[0].parentElement;\n        return parentEl.matches(\"figure\") ? parentEl.parentElement : parentEl;\n    },\n});\n\n/*\n * Abstract option to be extended by the ImageTools and BackgroundOptimize\n * options that handles all the common parts.\n */\nconst ImageHandlerOption = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    async willStart() {\n        const _super = this._super.bind(this);\n        await this._initializeImage();\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        const weightEl = document.createElement('span');\n        weightEl.classList.add('o_we_image_weight', 'o_we_tag', 'd-none');\n        weightEl.title = _t(\"Size\");\n        this.$weight = $(weightEl);\n        // Perform the loading of the image info synchronously in order to\n        // avoid an intermediate rendering of the Blocks tab during the\n        // loadImageInfo RPC that obtains the file size.\n        // This does not update the target.\n        await this._applyOptions(false);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUI() {\n        await this._super(...arguments);\n\n        if (this._filesize === undefined) {\n            this.$weight.addClass('d-none');\n            await this._applyOptions(false);\n        }\n        if (this._filesize !== undefined) {\n            this.$weight.text(`${this._filesize.toFixed(1)} kb`);\n            this.$weight.removeClass('d-none');\n            this._relocateWeightEl();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    selectFormat(previewMode, widgetValue, params) {\n        const values = widgetValue.split(' ');\n        const image = this._getImg();\n        image.dataset.resizeWidth = values[0];\n        if (image.dataset.shape) {\n            // If the image has a shape, modify its originalMimetype attribute.\n            image.dataset.originalMimetype = values[1];\n        } else {\n            // If the image does not have a shape, modify its mimetype\n            // attribute.\n            image.dataset.mimetype = values[1];\n        }\n        return this._applyOptions();\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async setQuality(previewMode, widgetValue, params) {\n        if (previewMode) {\n            return;\n        }\n        this._getImg().dataset.quality = widgetValue;\n        return this._applyOptions();\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    glFilter(previewMode, widgetValue, params) {\n        const dataset = this._getImg().dataset;\n        if (widgetValue) {\n            dataset.glFilter = widgetValue;\n        } else {\n            delete dataset.glFilter;\n        }\n        return this._applyOptions();\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    customFilter(previewMode, widgetValue, params) {\n        const img = this._getImg();\n        const {filterOptions} = img.dataset;\n        const {filterProperty} = params;\n        if (filterProperty === 'filterColor') {\n            widgetValue = normalizeColor(widgetValue);\n        }\n        const newOptions = Object.assign(JSON.parse(filterOptions || \"{}\"), {[filterProperty]: widgetValue});\n        img.dataset.filterOptions = JSON.stringify(newOptions);\n        return this._applyOptions();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeVisibility() {\n        const src = this._getImg().getAttribute('src');\n        return src && src !== '/';\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        const img = this._getImg();\n        const _super = this._super.bind(this);\n\n        // Make sure image is loaded because we need its naturalWidth\n        await new Promise((resolve, reject) => {\n            if (img.complete) {\n                resolve();\n                return;\n            }\n            img.addEventListener('load', resolve, {once: true});\n            img.addEventListener('error', resolve, {once: true});\n        });\n\n        switch (methodName) {\n            case 'selectFormat':\n                return img.naturalWidth + ' ' + this._getImageMimetype(img);\n            case 'setFilter':\n                return img.dataset.filter;\n            case 'glFilter':\n                return img.dataset.glFilter || \"\";\n            case 'setQuality':\n                return img.dataset.quality || 75;\n            case 'customFilter': {\n                const {filterProperty} = params;\n                const options = JSON.parse(img.dataset.filterOptions || \"{}\");\n                const defaultValue = filterProperty === 'blend' ? 'normal' : 0;\n                return options[filterProperty] || defaultValue;\n            }\n        }\n        return _super(...arguments);\n    },\n    /**\n     * @abstract\n     */\n    _relocateWeightEl() {},\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        const img = this._getImg();\n        if (!this.originalSrc || !this._isImageSupportedForProcessing(img)) {\n            return;\n        }\n        const $select = $(uiFragment).find('we-select[data-name=format_select_opt]');\n        (await this._computeAvailableFormats()).forEach(([value, [label, targetFormat]]) => {\n            $select.append(`<we-button data-select-format=\"${Math.round(value)} ${targetFormat}\" class=\"o_we_badge_at_end\">${label} <span class=\"badge rounded-pill text-bg-dark\">${targetFormat.split('/')[1]}</span></we-button>`);\n        });\n\n        if (!['image/jpeg', 'image/webp'].includes(this._getImageMimetype(img))) {\n            const optQuality = uiFragment.querySelector('we-range[data-set-quality]');\n            if (optQuality) {\n                optQuality.remove();\n            }\n        }\n    },\n    /**\n     * Returns a list of valid formats for a given image or an empty list if\n     * there is no mimetypeBeforeConversion data attribute on the image.\n     *\n     * @private\n     */\n    async _computeAvailableFormats() {\n        if (!this.mimetypeBeforeConversion) {\n            return [];\n        }\n        const img = this._getImg();\n        const original = await loadImage(this.originalSrc);\n        const maxWidth = img.dataset.width ? img.naturalWidth : original.naturalWidth;\n        const optimizedWidth = Math.min(maxWidth, this._computeMaxDisplayWidth());\n        this.optimizedWidth = optimizedWidth;\n        const widths = {\n            128: ['128px', 'image/webp'],\n            256: ['256px', 'image/webp'],\n            512: ['512px', 'image/webp'],\n            1024: ['1024px', 'image/webp'],\n            1920: ['1920px', 'image/webp'],\n        };\n        widths[img.naturalWidth] = [_t(\"%spx\", img.naturalWidth), 'image/webp'];\n        widths[optimizedWidth] = [_t(\"%spx (Suggested)\", optimizedWidth), 'image/webp'];\n        const mimetypeBeforeConversion = img.dataset.mimetypeBeforeConversion;\n        widths[maxWidth] = [_t(\"%spx (Original)\", maxWidth), mimetypeBeforeConversion];\n        if (mimetypeBeforeConversion !== \"image/webp\") {\n            // Avoid a key collision by subtracting 0.1 - putting the webp\n            // above the original format one of the same size.\n            widths[maxWidth - 0.1] = [_t(\"%spx\", maxWidth), 'image/webp'];\n        }\n        return Object.entries(widths)\n            .filter(([width]) => width <= maxWidth)\n            .sort(([v1], [v2]) => v1 - v2);\n    },\n    /**\n     * Applies all selected options on the original image.\n     *\n     * @private\n     * @param {boolean} [update=true] If this is false, this does not actually\n     *     modifies the image but only simulates the modifications on it to\n     *     be able to update the filesize UI.\n     */\n    async _applyOptions(update = true) {\n        const img = this._getImg();\n        if (!update && !(img && img.complete)) {\n            return;\n        }\n        if (!this._isImageSupportedForProcessing(img)) {\n            this.originalId = null;\n            this._filesize = undefined;\n            return;\n        }\n        // Do not apply modifications if there is no original src, since it is\n        // needed for it.\n        if (!img.dataset.originalSrc) {\n            delete img.dataset.mimetype;\n            return;\n        }\n        const dataURL = await applyModifications(img, {mimetype: this._getImageMimetype(img)});\n        this._filesize = getDataURLBinarySize(dataURL) / 1024;\n\n        if (update) {\n            img.classList.add('o_modified_image_to_save');\n            const loadedImg = await loadImage(dataURL, img);\n            this._applyImage(loadedImg);\n            // Also apply to carousel thumbnail if applicable.\n            weUtils.forwardToThumbnail(img);\n            return loadedImg;\n        }\n        return img;\n    },\n    /**\n     * Loads the image's attachment info.\n     *\n     * @private\n     */\n    async _loadImageInfo(attachmentSrc = '') {\n        const img = this._getImg();\n        await loadImageInfo(img, attachmentSrc);\n        if (!img.dataset.originalId) {\n            this.originalId = null;\n            this.originalSrc = null;\n            return;\n        }\n        this.originalId = img.dataset.originalId;\n        this.originalSrc = img.dataset.originalSrc;\n        this.mimetypeBeforeConversion = img.dataset.mimetypeBeforeConversion;\n    },\n    /**\n     * Sets the image's width to its suggested size.\n     *\n     * @private\n     */\n    async _autoOptimizeImage() {\n        await this._loadImageInfo();\n        await this._rerenderXML();\n        const img = this._getImg();\n        if (!['image/gif', 'image/svg+xml'].includes(img.dataset.mimetype)) {\n            // Convert to recommended format and width.\n            img.dataset.mimetype = 'image/webp';\n            img.dataset.resizeWidth = this.optimizedWidth;\n        } else if (img.dataset.shape && img.dataset.originalMimetype !== \"image/gif\") {\n            img.dataset.originalMimetype = \"image/webp\";\n            img.dataset.resizeWidth = this.optimizedWidth;\n        }\n        await this._applyOptions();\n        await this.updateUI();\n    },\n    /**\n     * Returns the image that is currently being modified.\n     *\n     * @private\n     * @abstract\n     * @returns {HTMLImageElement} the image to use for modifications\n     */\n    _getImg() {},\n    /**\n     * Computes the image's maximum display width.\n     *\n     * @private\n     * @abstract\n     * @returns {Int} the maximum width at which the image can be displayed\n     */\n    _computeMaxDisplayWidth() {},\n    /**\n     * Use the processed image when it's needed in the DOM.\n     *\n     * @private\n     * @abstract\n     * @param {HTMLImageElement} img\n     */\n    _applyImage(img) {},\n    /**\n     * @private\n     * @param {HTMLImageElement} img\n     * @returns {String} The right mimetype used to apply options on image.\n     */\n    _getImageMimetype(img) {\n        return img.dataset.mimetype;\n    },\n    /**\n     * @private\n     */\n    async _initializeImage() {\n        return this._loadImageInfo();\n    },\n     /**\n     * @private\n     * @param {HTMLImageElement} img\n     * @param {Boolean} [strict=false]\n     * @returns {Boolean}\n     */\n    _isImageSupportedForProcessing(img, strict = false) {\n        return isImageSupportedForProcessing(this._getImageMimetype(img), strict);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === \"format_select_opt\" && !this.mimetypeBeforeConversion) {\n            return false;\n        }\n        if (this._isImageProcessingWidget(widgetName, params)) {\n            const img = this._getImg();\n            return this._isImageSupportedForProcessing(img, true);\n        }\n        return isImageSupportedForStyle(this._getImg());\n    },\n    /**\n     * Indicates if an option should be applied only on supported mimetypes.\n     *\n     * @param {String} widgetName\n     * @param {Object} params\n     * @returns {Boolean}\n     */\n    _isImageProcessingWidget(widgetName, params) {\n        return params.optionsPossibleValues.glFilter\n            || 'customFilter' in params.optionsPossibleValues\n            || params.optionsPossibleValues.setQuality\n            || widgetName === 'format_select_opt';\n    },\n});\n\n/**\n * @param {Element} containerEl\n * @param {boolean} labelIsDimension - Optional display imgsize attribute instead of animated\n * @returns {Element}\n */\nconst _addAnimatedShapeLabel = function addAnimatedShapeLabel(containerEl, labelIsDimension = false) {\n    const labelEl = document.createElement('span');\n    labelEl.classList.add('o_we_shape_animated_label');\n    let labelStr = _t(\"Animated\");\n    const spanEl = document.createElement('span');\n    if (labelIsDimension) {\n        const dimensionIcon = document.createElement('i');\n        labelStr = containerEl.dataset.imgSize;\n        dimensionIcon.classList.add('fa', 'fa-expand');\n        labelEl.append(dimensionIcon);\n        spanEl.textContent = labelStr;\n    } else {\n        labelEl.textContent = labelStr[0];\n        spanEl.textContent = labelStr.substr(1);\n    }\n    labelEl.appendChild(spanEl);\n    containerEl.classList.add('position-relative');\n    containerEl.appendChild(labelEl);\n    return labelEl;\n};\n\n/**\n * Controls image width and quality.\n */\nregistry.ImageTools = ImageHandlerOption.extend({\n    MAX_SUGGESTED_WIDTH: 1920,\n\n    /**\n     * @constructor\n     */\n    init() {\n        this.shapeCache = {};\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start() {\n        this.$target.on('image_changed.ImageOptimization', this._onImageChanged.bind(this));\n        this.$target.on('image_cropped.ImageOptimization', this._onImageCropped.bind(this));\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$target.off('.ImageOptimization');\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Toggles the ratio of the image between 0:0 (no crop) and 1:1, in order to\n     * make it appear squared when needed (with `data-unstretch=true` shapes).\n     *\n     * @see this.selectClass for parameters\n     */\n    async removeStretch(previewMode, widgetValue, params) {\n        this.options.wysiwyg.odooEditor.historyPauseSteps();\n        this.trigger_up(\"disable_loading_effect\");\n        // Preserve the cursor to be able to replace the image afterwards.\n        const restoreCursor = preserveCursor(this.$target[0].ownerDocument);\n        const img = this._getImg();\n        const document = this.el.ownerDocument;\n        const imageCropWrapperElement = document.createElement(\"div\");\n        imageCropWrapperElement.classList.add(\"d-none\"); // Hiding the cropper.\n        document.body.append(imageCropWrapperElement);\n        const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {\n            activeOnStart: true,\n            media: img,\n            mimetype: this._getImageMimetype(img),\n        });\n        await imageCropWrapper.component.mountedPromise;\n        if (widgetValue) {\n            await imageCropWrapper.component.reset();\n        } else {\n            await imageCropWrapper.component.cropSquare(false);\n            if (isGif(this._getImageMimetype(img))) {\n                img.dataset[img.dataset.shape ? \"originalMimetype\" : \"mimetype\"] = \"image/png\";\n            }\n        }\n        await this._reapplyCurrentShape();\n        imageCropWrapper.destroy();\n        imageCropWrapperElement.remove();\n        restoreCursor();\n        this.trigger_up(\"enable_loading_effect\");\n        if (!widgetValue) {\n            await this._onImageCropped();\n        }\n        this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n    },\n    /**\n     * Displays the image cropping tools\n     *\n     * @see this.selectClass for parameters\n     */\n    async crop() {\n        this.trigger_up('disable_loading_effect');\n        const img = this._getImg();\n        const document = this.$el[0].ownerDocument;\n        const imageCropWrapperElement = document.createElement('div');\n        document.body.append(imageCropWrapperElement);\n        const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {\n            activeOnStart: true,\n            media: img,\n            mimetype: this._getImageMimetype(img),\n        });\n\n        await new Promise(resolve => {\n            this.$target.one('image_cropper_destroyed', async () => {\n                if (isGif(this._getImageMimetype(img))) {\n                    img.dataset[img.dataset.shape ? 'originalMimetype' : 'mimetype'] = 'image/png';\n                }\n                await this._reapplyCurrentShape();\n                resolve();\n            });\n        });\n        imageCropWrapperElement.remove();\n        imageCropWrapper.destroy();\n        this.trigger_up('enable_loading_effect');\n    },\n    /**\n     * Displays the image transformation tools\n     *\n     * @see this.selectClass for parameters\n     */\n    async transform() {\n        this.trigger_up('hide_overlay');\n        this.trigger_up('disable_loading_effect');\n\n        const document = this.$target[0].ownerDocument;\n        const playState = this.$target[0].style.animationPlayState;\n        const transition = this.$target[0].style.transition;\n        this.$target.transfo({document});\n        const destroyTransfo = () => {\n            this.$target.transfo('destroy');\n            $(document).off('mousedown', mousedown);\n            window.document.removeEventListener('keydown', keydown);\n        }\n        const mousedown = mousedownEvent => {\n            if (!$(mousedownEvent.target).closest('.transfo-container').length) {\n                destroyTransfo();\n                // Restore animation css properties potentially affected by the\n                // jQuery transfo plugin.\n                this.$target[0].style.animationPlayState = playState;\n                this.$target[0].style.transition = transition;\n            }\n        };\n        $(document).on('mousedown', mousedown);\n        const keydown = keydownEvent => {\n            if (keydownEvent.key === 'Escape') {\n                keydownEvent.stopImmediatePropagation();\n                destroyTransfo();\n            }\n        };\n        window.document.addEventListener('keydown', keydown);\n\n        await new Promise(resolve => {\n            document.addEventListener('mouseup', resolve, {once: true});\n        });\n        this.trigger_up('enable_loading_effect');\n    },\n    /**\n     * Resets the image cropping\n     *\n     * @see this.selectClass for parameters\n     */\n    async resetCrop() {\n        const img = this._getImg();\n\n        // Mount the ImageCrop to call the reset method. As we need the state of\n        // the component to be mounted before calling reset, mount it\n        // temporarily into the body.\n        const imageCropWrapperElement = document.createElement('div');\n        imageCropWrapperElement.classList.add(\"d-none\"); // Hiding the cropper.\n        document.body.append(imageCropWrapperElement);\n        const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {\n            activeOnStart: true,\n            media: img,\n            mimetype: this._getImageMimetype(img),\n        });\n        await imageCropWrapper.component.mountedPromise;\n        await imageCropWrapper.component.reset();\n        imageCropWrapper.destroy();\n        imageCropWrapperElement.remove();\n\n        await this._reapplyCurrentShape();\n    },\n    /**\n     * Resets the image rotation and translation\n     *\n     * @see this.selectClass for parameters\n     */\n    async resetTransform() {\n        this.$target\n            .attr('style', (this.$target.attr('style') || '')\n            .replace(/[^;]*transform[\\w:]*;?/g, ''));\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async setImgShape(previewMode, widgetValue, params) {\n        this.trigger_up(\"disable_loading_effect\");\n        const img = this._getImg();\n        let clonedImgEl;\n\n        // If the shape needs the image to be square (1:1 ratio) and if not\n        // already the case, crop the image before applying the shape.\n        const isCropRequired = params.unstretch;\n        if ((isCropRequired && img.dataset.aspectRatio !== \"1/1\" && previewMode !== \"reset\")\n                || this.hasCroppedPreview) {\n            // Preserve the cursor to be able to replace the image afterwards.\n            const restoreCursor = preserveCursor(this.$target[0].ownerDocument);\n            // Replace the image by its clone to avoid flickering.\n            clonedImgEl = img.cloneNode(true);\n            img.insertAdjacentElement(\"afterend\", clonedImgEl);\n            img.classList.add(\"d-none\");\n\n            const document = this.el.ownerDocument;\n            const imageCropWrapperElement = document.createElement(\"div\");\n            imageCropWrapperElement.classList.add(\"d-none\"); // Hiding the cropper.\n            document.body.append(imageCropWrapperElement);\n            const imageCropWrapper = await attachComponent(this, imageCropWrapperElement, ImageCrop, {\n                activeOnStart: true,\n                media: img,\n                mimetype: this._getImageMimetype(img),\n            });\n            await imageCropWrapper.component.mountedPromise;\n            await imageCropWrapper.component.cropSquare(previewMode);\n            if (previewMode === false) {\n                if (isGif(this._getImageMimetype(img))) {\n                    img.dataset[img.dataset.shape ? \"originalMimetype\" : \"mimetype\"] = \"image/png\";\n                }\n                this.isImageCropped = true;\n            }\n            await this._reapplyCurrentShape();\n            imageCropWrapper.destroy();\n            imageCropWrapperElement.remove();\n\n            restoreCursor();\n\n            if (previewMode === true) {\n                this.hasCroppedPreview = true;\n            } else {\n                delete this.hasCroppedPreview;\n            }\n        }\n        // Re-rendering the options after selecting a \"cropping\" shape.\n        if (this.isImageCropped && previewMode === \"reset\") {\n            delete this.isImageCropped;\n            await this._onImageCropped();\n        }\n\n        const saveData = previewMode === false;\n        if (img.dataset.hoverEffect && !widgetValue) {\n            // When a shape is removed and there is a hover effect on the\n            // image, we then place the \"Square\" shape as the default because a\n            // shape is required for the hover effects to work.\n            const shapeImgSquareWidget = this._requestUserValueWidgets(\"shape_img_square_opt\")[0];\n            widgetValue = shapeImgSquareWidget.getActiveValue(\"setImgShape\");\n        }\n        if (widgetValue) {\n            await this._loadShape(widgetValue);\n            if (previewMode === 'reset' && img.dataset.shapeColors) {\n                // When we reset the shape we need to reapply the colors the\n                // user had selected.\n                await this._applyShapeAndColors(false, img.dataset.shapeColors.split(';'));\n            } else {\n                // If the preview mode === false we want to save the colors\n                // as the user chose their shape\n                await this._applyShapeAndColors(saveData);\n                if (saveData && img.dataset.mimetype !== 'image/svg+xml') {\n                    img.dataset.originalMimetype = img.dataset.mimetype;\n                    img.dataset.mimetype = 'image/svg+xml';\n                }\n                // When the user selects a shape, we remove the data attributes\n                // that are not compatible with this shape.\n                if (saveData) {\n                    if (!this._isTransformableShape()) {\n                        delete img.dataset.shapeFlip;\n                        delete img.dataset.shapeRotate;\n                    }\n                    if (!this._canHaveHoverEffect()) {\n                        delete img.dataset.hoverEffect;\n                        delete img.dataset.hoverEffectColor;\n                        delete img.dataset.hoverEffectStrokeWidth;\n                        delete img.dataset.hoverEffectIntensity;\n                        img.classList.remove(\"o_animate_on_hover\");\n                    }\n                    if (!this._isAnimatedShape()) {\n                        delete img.dataset.shapeAnimationSpeed;\n                    }\n                }\n            }\n        } else {\n            // Re-applying the modifications and deleting the shapes\n            img.src = await applyModifications(img, {mimetype: this._getImageMimetype(img)});\n            delete img.dataset.shape;\n            delete img.dataset.shapeColors;\n            delete img.dataset.fileName;\n            delete img.dataset.shapeFlip;\n            delete img.dataset.shapeRotate;\n            delete img.dataset.shapeAnimationSpeed;\n            if (saveData) {\n                img.dataset.mimetype = img.dataset.originalMimetype;\n                delete img.dataset.originalMimetype;\n            }\n            // Also apply to carousel thumbnail if applicable.\n            weUtils.forwardToThumbnail(img);\n        }\n        img.classList.add('o_modified_image_to_save');\n        // Remove the image clone, if any.\n        if (clonedImgEl) {\n            clonedImgEl.remove();\n            img.classList.remove(\"d-none\");\n        }\n        this.trigger_up(\"enable_loading_effect\");\n    },\n    /**\n     * Handles color assignment on the shape. Widget is a color picker.\n     * If no value, we reset to the current color palette.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeColor(previewMode, widgetValue, params) {\n        const img = this._getImg();\n        const newColorId = parseInt(params.colorId);\n        const oldColors = img.dataset.shapeColors.split(';');\n        const newColors = oldColors.slice(0);\n        newColors[newColorId] = this._getCSSColorValue(widgetValue === '' ? `o-color-${(newColorId + 1)}` : widgetValue);\n        await this._applyShapeAndColors(true, newColors);\n        img.classList.add('o_modified_image_to_save');\n    },\n    /**\n     * Flips the image shape horizontally.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeFlipX(previewMode, widgetValue, params) {\n        await this._setImgShapeFlip(\"x\");\n    },\n    /**\n     * Flips the image shape vertically.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeFlipY(previewMode, widgetValue, params) {\n        await this._setImgShapeFlip(\"y\");\n    },\n    /**\n     * Rotates the image shape 90 degrees to the left.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeRotateLeft(previewMode, widgetValue, params) {\n        await this._setImgShapeRotate(-90);\n    },\n    /**\n     * Rotates the image shape 90 degrees to the right.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeRotateRight(previewMode, widgetValue, params) {\n        await this._setImgShapeRotate(90);\n    },\n    /**\n     * Sets the hover effects of the image shape.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setImgShapeHoverEffect(previewMode, widgetValue, params) {\n        const imgEl = this._getImg();\n        if (previewMode !== \"reset\") {\n            this.prevHoverEffectColor = imgEl.dataset.hoverEffectColor;\n            this.prevHoverEffectIntensity = imgEl.dataset.hoverEffectIntensity;\n            this.prevHoverEffectStrokeWidth = imgEl.dataset.hoverEffectStrokeWidth;\n        }\n        delete imgEl.dataset.hoverEffectColor;\n        delete imgEl.dataset.hoverEffectIntensity;\n        delete imgEl.dataset.hoverEffectStrokeWidth;\n        if (previewMode === true) {\n            if (params.name === \"hover_effect_overlay_opt\") {\n                imgEl.dataset.hoverEffectColor = this._getCSSColorValue(\"black-25\");\n            } else if (params.name === \"hover_effect_outline_opt\") {\n                imgEl.dataset.hoverEffectColor = this._getCSSColorValue(\"primary\");\n                imgEl.dataset.hoverEffectStrokeWidth = 10;\n            } else {\n                imgEl.dataset.hoverEffectIntensity = 20;\n                if (params.name !== \"hover_effect_mirror_blur_opt\") {\n                    imgEl.dataset.hoverEffectColor = \"rgba(0, 0, 0, 0)\";\n                }\n            }\n        } else {\n            if (this.prevHoverEffectColor) {\n                imgEl.dataset.hoverEffectColor = this.prevHoverEffectColor;\n            }\n            if (this.prevHoverEffectIntensity) {\n                imgEl.dataset.hoverEffectIntensity = this.prevHoverEffectIntensity;\n            }\n            if (this.prevHoverEffectStrokeWidth) {\n                imgEl.dataset.hoverEffectStrokeWidth = this.prevHoverEffectStrokeWidth;\n            }\n        }\n        await this._reapplyCurrentShape();\n        // When the hover effects are first activated from the \"animationMode\"\n        // function of the \"WebsiteAnimate\" class, the history was paused to\n        // avoid recording intermediate steps. That's why we unpause it here.\n        if (this.firstHoverEffect) {\n            this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n            delete this.firstHoverEffect;\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if ([\"shapeAnimationSpeed\", \"hoverEffectIntensity\", \"hoverEffectStrokeWidth\"].includes(params.attributeName)) {\n            await this._reapplyCurrentShape();\n        }\n    },\n    /**\n     * Sets the color of hover effects.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setHoverEffectColor(previewMode, widgetValue, params) {\n        const img = this._getImg();\n        let defaultColor = \"rgba(0, 0, 0, 0)\";\n        if (img.dataset.hoverEffect === \"overlay\") {\n            defaultColor = \"black-25\";\n        } else if (img.dataset.hoverEffect === \"outline\") {\n            defaultColor = \"primary\";\n        }\n        img.dataset.hoverEffectColor = this._getCSSColorValue(widgetValue || defaultColor);\n        await this._reapplyCurrentShape();\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    notify(name) {\n        if (name === \"enable_hover_effect\") {\n            this.trigger_up(\"snippet_edition_request\", {exec: () => {\n                // Add the \"square\" shape to the image if it has no shape\n                // because the \"hover effects\" need a shape to work.\n                const imgEl = this._getImg();\n                const shapeName = imgEl.dataset.shape?.split(\"/\")[2];\n                if (!shapeName) {\n                    const shapeImgSquareWidget = this._requestUserValueWidgets(\"shape_img_square_opt\")[0];\n                    shapeImgSquareWidget.enable();\n                    shapeImgSquareWidget.getParent().close(); // FIXME remove this ugly hack asap\n                }\n                // Add the \"Overlay\" hover effect to the shape.\n                this.firstHoverEffect = true;\n                const hoverEffectOverlayWidget = this._requestUserValueWidgets(\"hover_effect_overlay_opt\")[0];\n                hoverEffectOverlayWidget.enable();\n                hoverEffectOverlayWidget.getParent().close(); // FIXME remove this ugly hack asap\n            }});\n        } else if (name === \"disable_hover_effect\") {\n            this._disableHoverEffect();\n        } else {\n            this._super(...arguments);\n        }\n    },\n    /**\n     * @override\n     */\n    async updateUI() {\n        await this._super(...arguments);\n        // Adapts the colorpicker label according to the selected \"On Hover\"\n        // animation.\n        const hoverEffectName = this.$target[0].dataset.hoverEffect;\n        if (hoverEffectName) {\n            const hoverEffectColorWidget = this.findWidget(\"hover_effect_color_opt\");\n            const needToAdaptLabel = [\"image_zoom_in\", \"image_zoom_out\", \"dolly_zoom\"].includes(hoverEffectName);\n            const labelEl = hoverEffectColorWidget.el.querySelector(\"we-title\");\n            if (!this._originalHoverEffectColorLabel) {\n                this._originalHoverEffectColorLabel = labelEl.textContent;\n            }\n            labelEl.textContent = needToAdaptLabel\n                ? _t(\"Overlay\")\n                : this._originalHoverEffectColorLabel;\n        }\n        // Move the \"hover effects\" options to the 'websiteAnimate' options.\n        const hoverEffectsOptionsEl = this.$el[0].querySelector(\"#o_hover_effects_options\");\n        const animationEffectWidget = this._requestUserValueWidgets(\"animation_effect_opt\")[0];\n        if (hoverEffectsOptionsEl && animationEffectWidget) {\n            animationEffectWidget.getParent().$el[0].append(hoverEffectsOptionsEl);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n\ufffc    * @private\n\ufffc    */\n    _isTransformed() {\n        return this.$target.is('[style*=\"transform\"]');\n    },\n    /**\n\ufffc    * @private\n\ufffc    */\n    _isCropped() {\n        return this.$target.hasClass('o_we_image_cropped');\n    },\n    /**\n     * @override\n     */\n    async _applyOptions() {\n        const img = await this._super(...arguments);\n        if (img && img.dataset.shape) {\n            await this._loadShape(img.dataset.shape);\n            if (/^data:/.test(img.src)) {\n                // Reapplying the shape\n                await this._applyShapeAndColors(true, (img.dataset.shapeColors && img.dataset.shapeColors.split(';')));\n            }\n        }\n        return img;\n    },\n    /**\n     * Loads the shape into cache if not already and sets it in the dataset of\n     * the img\n     *\n     * @param {string} shapeName identifier of the shape\n     */\n    async _loadShape(shapeName) {\n        const [module, directory, fileName] = shapeName.split('/');\n        let shape = this.shapeCache[fileName];\n        if (!shape) {\n            const shapeURL = `/${encodeURIComponent(module)}/static/image_shapes/${encodeURIComponent(directory)}/${encodeURIComponent(fileName)}.svg`;\n            shape = await (await fetch(shapeURL)).text();\n            this.shapeCache[fileName] = shape;\n        }\n        this._getImg().dataset.shape = shapeName;\n    },\n\n    /**\n     * Applies the shape in img.dataset.shape and replaces the previous hex\n     * color values with new ones or current theme\n     * ones then calls _writeShape()\n     *\n     * @param {boolean} save true if the colors need to be saved in the\n     * data-attribute\n     * @param {string[]} [newColors] Array of HEX color code, default\n     * theme colors are applied if not supplied\n     */\n    async _applyShapeAndColors(save, newColors) {\n        const img = this._getImg();\n        let shape = this.shapeCache[img.dataset.shape.split('/')[2]];\n\n        // Map the default palette colors to an array if the shape includes them\n        // If they do not map a NULL, this way we know if a default color is in\n        // the shape\n        const oldColors = Object.values(DEFAULT_PALETTE).map(color => shape.includes(color) ? color : null);\n        if (!newColors) {\n            // If we do not have newColors, we still replace the default\n            // shape's colors by the current palette's\n            newColors = oldColors.map((color, i) => color !== null ? this._getCSSColorValue(`o-color-${(i + 1)}`) : null);\n        }\n        newColors.forEach((color, i) => shape = shape.replace(new RegExp(oldColors[i], 'g'), this._getCSSColorValue(color)));\n        await this._writeShape(shape);\n        if (save) {\n            img.dataset.shapeColors = newColors.join(';');\n        }\n        // Also apply to carousel thumbnail if applicable.\n        weUtils.forwardToThumbnail(img);\n    },\n    /**\n     * Replace animation durations in SVG and CSS with modified values.\n     *\n     * This function takes a ratio and an SVG string containing animations. It\n     * uses regular expressions to find and replace the duration values in both\n     * CSS animation rules and SVG duration attributes based on the provided\n     * ratio.\n     *\n     * @param {number} speed The speed used to calculate the new animation\n     *                       durations. If speed is 0.0, the original\n     *                       durations are preserved.\n     * @param {string} svg The SVG string containing animations.\n     * @returns {string} The modified SVG string with updated animation\n     *                   durations.\n     */\n    _replaceAnimationDuration(speed, svg) {\n        const ratio = (speed >= 0.0 ? 1.0 + speed : 1.0 / (1.0 - speed)).toFixed(3);\n        // Callback for CSS 'animation' and 'animation-duration' declarations\n        function callbackCssAnimationRule(match, declaration, value, unit, separator) {\n            value = parseFloat(value) / (ratio ? ratio : 1);\n            return `${declaration}${value}${unit}${separator}`;\n        }\n\n        // Callback function for handling the 'dur' SVG attribute timecount\n        // value in accordance with the SMIL animation specification (e.g., 4s,\n        // 2ms). If no unit is provided, seconds are implied.\n        function callbackSvgDurTimecountVal(match, attribute_name, value, unit) {\n            value = parseFloat(value) / (ratio ? ratio : 1);\n            return `${attribute_name}${value}${unit ? unit : 's'}\"`\n        }\n\n        // Applying regex substitutions to modify animation speed in the 'svg'\n        // variable.\n        svg = svg.replace(CSS_ANIMATION_RULE_REGEX, callbackCssAnimationRule);\n        svg = svg.replace(SVG_DUR_TIMECOUNT_VAL_REGEX, callbackSvgDurTimecountVal);\n        if (CSS_ANIMATION_RATIO_REGEX.test(svg)) {\n            // Replace the CSS --animation_ratio variable for future purpose.\n            svg = svg.replace(CSS_ANIMATION_RATIO_REGEX, `--animation_ratio: ${ratio};`);\n        } else {\n            // Add the style tag with the root variable --animation ratio for\n            // future purpose.\n            const regex = /<svg .*>/m;\n            const subst = `$&\\n\\t<style>\\n\\t\\t:root { \\n\\t\\t\\t--animation_ratio: ${ratio};\\n\\t\\t}\\n\\t</style>`;\n            svg = svg.replace(regex, subst);\n        }\n        return svg;\n    },\n    /**\n     * Sets the image in the supplied SVG and replace the src with a dataURL\n     *\n     * @param {string} svgText svg file as text\n     * @returns {Promise} resolved once the svg is properly loaded\n     * in the document\n     */\n    async _writeShape(svgText) {\n        const img = this._getImg();\n        let needToRefreshPublicWidgets = false;\n        let hasHoverEffect = false;\n\n        // Add shape animations on hover.\n        if (img.dataset.hoverEffect && this._canHaveHoverEffect()) {\n            // The \"ImageShapeHoverEffet\" public widget needs to restart\n            // (e.g. image replacement).\n            needToRefreshPublicWidgets = true;\n            hasHoverEffect = true;\n        }\n\n        const dataURL = await this.computeShape(svgText, img);\n\n        let clonedImgEl = null;\n        if (hasHoverEffect) {\n            // This is useful during hover effects previews. Without this, in\n            // Chrome, the 'mouse out' animation is triggered very briefly when\n            // previewMode === 'reset' (when transitioning from one hover effect\n            // to another), causing a visual glitch. To avoid this, we hide the\n            // image with its clone when the source is set.\n            clonedImgEl = img.cloneNode(true);\n            this.options.wysiwyg.odooEditor.observerUnactive(\"addClonedImgForHoverEffectPreview\");\n            img.classList.add(\"d-none\");\n            img.insertAdjacentElement(\"afterend\", clonedImgEl);\n            this.options.wysiwyg.odooEditor.observerActive(\"addClonedImgForHoverEffectPreview\");\n        }\n        const loadedImg = await loadImage(dataURL, img);\n        if (hasHoverEffect) {\n            this.options.wysiwyg.odooEditor.observerUnactive(\"removeClonedImgForHoverEffectPreview\");\n            clonedImgEl.remove();\n            img.classList.remove(\"d-none\");\n            this.options.wysiwyg.odooEditor.observerActive(\"removeClonedImgForHoverEffectPreview\");\n        }\n        if (needToRefreshPublicWidgets) {\n            await this._refreshPublicWidgets();\n        }\n        return loadedImg;\n    },\n    /**\n     * Sets the image in the supplied SVG and replace the src with a dataURL\n     *\n     * @param {string} svgText svg text file\n     * @param img JQuery image\n     * @returns {Promise} resolved once the svg is properly loaded\n     * in the document\n     */\n    async computeShape(svgText, img) {\n        const initialImageWidth = img.naturalWidth;\n\n        // Apply the right animation speed if there is an animated shape.\n        const shapeAnimationSpeed = Number(img.dataset.shapeAnimationSpeed) || 0;\n        if (shapeAnimationSpeed) {\n            svgText = this._replaceAnimationDuration(shapeAnimationSpeed, svgText);\n        }\n\n        const svg = new DOMParser().parseFromString(svgText, 'image/svg+xml').documentElement;\n\n        // Modifies the SVG according to the \"flip\" or/and \"rotate\" options.\n        const shapeFlip = img.dataset.shapeFlip || \"\";\n        const shapeRotate = img.dataset.shapeRotate || 0;\n        if ((shapeFlip || shapeRotate) && this._isTransformableShape()) {\n            let shapeTransformValues = [];\n            if (shapeFlip) { // Possible values => \"x\", \"y\", \"xy\"\n                shapeTransformValues.push(`scale${shapeFlip === \"x\" ? \"X\" : shapeFlip === \"y\" ? \"Y\" : \"\"}(-1)`);\n            }\n            if (shapeRotate) { // Possible values => \"90\", \"180\", \"270\"\n                shapeTransformValues.push(`rotate(${shapeRotate}deg)`);\n            }\n            // \"transform-origin: center;\" does not work on \"#filterPath\". But\n            // since its dimension is 1px * 1px the following solution works.\n            const transformOrigin = \"transform-origin: 0.5px 0.5px;\";\n            // Applies the transformation values to the path used to create a\n            // mask over the SVG image.\n            svg.querySelector(\"#filterPath\").setAttribute(\"style\", `transform: ${shapeTransformValues.join(\" \")}; ${transformOrigin}`);\n        }\n\n        // Add shape animations on hover.\n        if (img.dataset.hoverEffect && this._canHaveHoverEffect()) {\n            this._addImageShapeHoverEffect(svg, img);\n        }\n\n        const svgAspectRatio = parseInt(svg.getAttribute('width')) / parseInt(svg.getAttribute('height'));\n        // We will store the image in base64 inside the SVG.\n        // applyModifications will return a dataURL with the current filters\n        // and size options.\n        const options = {\n            mimetype: this._getImageMimetype(img),\n            perspective: svg.dataset.imgPerspective || null,\n            imgAspectRatio: svg.dataset.imgAspectRatio || null,\n            svgAspectRatio: svgAspectRatio,\n        };\n        const imgDataURL = await applyModifications(img, options);\n        svg.removeChild(svg.querySelector('#preview'));\n        svg.querySelectorAll(\"image\").forEach(image => {\n            image.setAttribute(\"xlink:href\", imgDataURL);\n        });\n        // Force natural width & height (note: loading the original image is\n        // needed for Safari where natural width & height of SVG does not return\n        // the correct values).\n        const originalImage = await loadImage(imgDataURL);\n        // If the svg forces the size of the shape we still want to have the resized\n        // width\n        if (!svg.dataset.forcedSize) {\n            svg.setAttribute('width', originalImage.naturalWidth);\n            svg.setAttribute('height', originalImage.naturalHeight);\n        } else {\n            const imageWidth = Math.trunc(img.dataset.resizeWidth || img.dataset.width || initialImageWidth);\n            const newHeight = imageWidth / svgAspectRatio;\n            svg.setAttribute('width', imageWidth);\n            svg.setAttribute('height', newHeight);\n        }\n        // Transform the current SVG in a base64 file to be saved by the server\n        const blob = new Blob([svg.outerHTML], {\n            type: 'image/svg+xml',\n        });\n        const dataURL = await createDataURL(blob);\n        const imgFilename = (img.dataset.originalSrc.split('/').pop()).split('.')[0];\n        img.dataset.fileName = `${imgFilename}.svg`;\n        return dataURL;\n    },\n    /**\n     * @override\n     */\n    _computeMaxDisplayWidth() {\n        const img = this._getImg();\n        const computedStyles = window.getComputedStyle(img);\n        const displayWidth = parseFloat(computedStyles.getPropertyValue('width'));\n        const gutterWidth = parseFloat(computedStyles.getPropertyValue('--o-grid-gutter-width')) || 30;\n\n        // For the logos we don't want to suggest a width too small.\n        if (this.$target[0].closest('nav')) {\n            return Math.round(Math.min(displayWidth * 3, this.MAX_SUGGESTED_WIDTH));\n        // If the image is in a container(-small), it might get bigger on\n        // smaller screens. So we suggest the width of the current image unless\n        // it is smaller than the size of the container on the md breapoint\n        // (which is where our bootstrap columns fallback to full container\n        // width since we only use col-lg-* in Odoo).\n        } else if (img.closest('.container, .o_container_small')) {\n            const mdContainerMaxWidth = parseFloat(computedStyles.getPropertyValue('--o-md-container-max-width')) || 720;\n            const mdContainerInnerWidth = mdContainerMaxWidth - gutterWidth;\n            return Math.round(clamp(displayWidth, mdContainerInnerWidth, this.MAX_SUGGESTED_WIDTH));\n        // If the image is displayed in a container-fluid, it might also get\n        // bigger on smaller screens. The same way, we suggest the width of the\n        // current image unless it is smaller than the max size of the container\n        // on the md breakpoint (which is the LG breakpoint since the container\n        // fluid is full-width).\n        } else if (img.closest('.container-fluid')) {\n            const lgBp = parseFloat(computedStyles.getPropertyValue('--breakpoint-lg')) || 992;\n            const mdContainerFluidMaxInnerWidth = lgBp - gutterWidth;\n            return Math.round(clamp(displayWidth, mdContainerFluidMaxInnerWidth, this.MAX_SUGGESTED_WIDTH));\n        }\n        // If it's not in a container, it's probably not going to change size\n        // depending on breakpoints. We still keep a margin safety.\n        return Math.round(Math.min(displayWidth * 1.5, this.MAX_SUGGESTED_WIDTH));\n    },\n    /**\n     * @override\n     */\n    _getImg() {\n        return this.$target[0];\n    },\n    /**\n     * @override\n     */\n    _relocateWeightEl() {\n        const leftPanelEl = this.$overlay.data('$optionsSection')[0];\n        const titleTextEl = leftPanelEl.querySelector('we-title > span');\n        this.$weight.appendTo(titleTextEl);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName.startsWith('img-shape-color')) {\n            const img = this._getImg();\n            const shapeName = img.dataset.shape;\n            const shapeColors = img.dataset.shapeColors;\n            if (!shapeName || !shapeColors) {\n                return false;\n            }\n            const colors = img.dataset.shapeColors.split(';');\n            return colors[parseInt(params.colorId)];\n        }\n        if (widgetName === \"shape_anim_speed_opt\") {\n            return this._isAnimatedShape();\n        }\n        if (params.optionsPossibleValues.resetTransform) {\n            return this._isTransformed();\n        }\n        if (params.optionsPossibleValues.resetCrop) {\n            return this._isCropped();\n        }\n        if (params.optionsPossibleValues.crop) {\n            const img = this._getImg();\n            return isImageSupportedForStyle(img) || this._isImageSupportedForProcessing(img);\n        }\n        if ([\"img_shape_transform_flip_x_opt\", \"img_shape_transform_flip_y_opt\",\n            \"img_shape_transform_rotate_x_opt\", \"img_shape_transform_rotate_y_opt\"].includes(params.name)) {\n            return this._isTransformableShape();\n        }\n        if (widgetName === \"hover_effect_none_opt\") {\n            // The hover effects are removed with the \"WebsiteAnimate\" animation\n            // selector so this option should not be visible.\n            return false;\n        }\n        if (params.optionsPossibleValues.setImgShapeHoverEffect) {\n            const imgEl = this._getImg();\n            return imgEl.classList.contains(\"o_animate_on_hover\") && this._canHaveHoverEffect();\n        }\n        // If \"Description\" or \"Tooltip\" options.\n        if ([\"alt\", \"title\"].includes(params.attributeName)) {\n            return isImageSupportedForStyle(this._getImg());\n        }\n        // The \"Square\" shape is only used for hover effects. It is\n        // automatically set when there is an hover effect and no shape is\n        // chosen by the user. This shape is always hidden in the shape select.\n        if (widgetName === \"shape_img_square_opt\") {\n            return false;\n        }\n        if (widgetName === \"remove_img_shape_opt\") {\n            // Do not show the \"remove shape\" button when the \"square\" shape is\n            // enable. The \"square\" shape is only enable when there is a hover\n            // effect and it is always hidden in the shape select.\n            const shapeImgSquareWidget = this._requestUserValueWidgets(\"shape_img_square_opt\")[0];\n            return !shapeImgSquareWidget.isActive();\n        }\n        if (widgetName === \"toggle_stretch_opt\") {\n            return this._isCropRequired();\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'selectStyle': {\n                if (params.cssProperty === 'width') {\n                    // TODO check how to handle this the right way (here using\n                    // inline style instead of computed because of the messy\n                    // %-px convertion and the messy auto keyword).\n                    const width = this.$target[0].style.width.trim();\n                    if (width[width.length - 1] === '%') {\n                        return `${parseInt(width)}%`;\n                    }\n                    return '';\n                }\n                break;\n            }\n            case 'transform': {\n                return this._isTransformed() ? 'true' : '';\n            }\n            case 'crop': {\n                return this._isCropped() ? 'true' : '';\n            }\n            case 'setImgShape': {\n                return this._getImg().dataset.shape || '';\n            }\n            case 'setImgShapeColor': {\n                const img = this._getImg();\n                return (img.dataset.shapeColors && img.dataset.shapeColors.split(';')[parseInt(params.colorId)]) || '';\n            }\n            case 'setImgShapeFlipX': {\n                const imgEl = this._getImg();\n                return imgEl.dataset.shapeFlip?.includes(\"x\") || \"\";\n            }\n            case 'setImgShapeFlipY': {\n                const imgEl = this._getImg();\n                return imgEl.dataset.shapeFlip?.includes(\"y\") || \"\";\n            }\n            case 'setHoverEffectColor': {\n                const imgEl = this._getImg();\n                return imgEl.dataset.hoverEffectColor || \"\";\n            }\n            case \"removeStretch\": {\n                const imgEl = this._getImg();\n                return imgEl.dataset.aspectRatio !== \"1/1\";\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Appends the SVG as an image.\n     * Due to the nature of image_shapes' SVGs, it is easier to render them as\n     * img compared to appending their content to the DOM\n     * (which is what the current data-img does)\n     *\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        await this._super(...arguments);\n        uiFragment.querySelectorAll('we-select-page we-button[data-set-img-shape]').forEach(btn => {\n            const image = document.createElement('img');\n            const [moduleName, directory, shapeName] = btn.dataset.setImgShape.split('/');\n            image.src = `/${encodeURIComponent(moduleName)}/static/image_shapes/${encodeURIComponent(directory)}/${encodeURIComponent(shapeName)}.svg`;\n            $(btn).prepend(image);\n\n            if (btn.dataset.animated) {\n                _addAnimatedShapeLabel(btn);\n            } else if (btn.dataset.imgSize) {\n                _addAnimatedShapeLabel(btn, true);\n            }\n        });\n    },\n    /**\n     * @override\n     */\n    _getImageMimetype(img) {\n        if (img.dataset.shape && img.dataset.originalMimetype) {\n            return img.dataset.originalMimetype;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Gets the CSS value of a color variable name so it can be used on shapes.\n     *\n     * @param {string} color\n     * @returns {string}\n     */\n    _getCSSColorValue(color) {\n        if (!color || isCSSColor(color)) {\n            return color;\n        }\n        return weUtils.getCSSVariableValue(color);\n    },\n    /**\n     * Overridden to set attachment data on theme images (with default shapes).\n     *\n     * @override\n     * @private\n     */\n    async _initializeImage() {\n        const _super = this._super.bind(this);\n        let img = this._getImg();\n\n        // Check first if the `src` and eventual `data-original-src` attributes\n        // are correct (i.e. the await are not rejected), as they may have been\n        // wrongly hardcoded in some templates.\n        let checkedAttribute = 'src';\n        try {\n            await loadImage(img.src);\n            if (img.dataset.originalSrc) {\n                checkedAttribute = 'originalSrc';\n                await loadImage(img.dataset.originalSrc);\n            }\n        } catch {\n            if (checkedAttribute === 'src') {\n                // If `src` does not exist, replace the image by a placeholder.\n                Object.keys(img.dataset).forEach(key => delete img.dataset[key]);\n                img.dataset.mimetype = 'image/png';\n                const newSrc = '/web/image/web.image_placeholder';\n                img = await loadImage(newSrc, img);\n                return this._loadImageInfo(newSrc);\n            } else {\n                // If `data-original-src` does not exist, remove the `data-\n                // original-*` attributes (they will be set correctly afterwards\n                // in `_loadImageInfo`).\n                delete img.dataset.originalId;\n                delete img.dataset.originalSrc;\n                delete img.dataset.originalMimetype;\n            }\n        }\n\n        let match = img.src.match(/\\/web_editor\\/image_shape\\/(\\w+\\.\\w+)/);\n        if (img.dataset.shape && match) {\n            match = match[1];\n            if (match.endsWith(\"_perspective\")) {\n                // As an image might already have been modified with a\n                // perspective for some customized snippets in themes. We need\n                // to find the original image to set the 'data-original-src'\n                // attribute.\n                match = match.slice(0, -12);\n            }\n            return this._loadImageInfo(`/web/image/${encodeURIComponent(match)}`);\n        }\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     * @private\n     */\n    async _loadImageInfo() {\n        await this._super(...arguments);\n        const img = this._getImg();\n        if (img.dataset.shape) {\n            if (img.dataset.mimetype !== \"image/svg+xml\") {\n                img.dataset.originalMimetype = img.dataset.mimetype;\n            }\n            if (!this._isImageSupportedForProcessing(img)) {\n                delete img.dataset.shape;\n                delete img.dataset.shapeColors;\n                delete img.dataset.fileName;\n                delete img.dataset.originalMimetype;\n                delete img.dataset.shapeFlip;\n                delete img.dataset.shapeRotate;\n                delete img.dataset.hoverEffect;\n                delete img.dataset.hoverEffectColor;\n                delete img.dataset.hoverEffectStrokeWidth;\n                delete img.dataset.hoverEffectIntensity;\n                img.classList.remove(\"o_animate_on_hover\");\n                delete img.dataset.shapeAnimationSpeed;\n                return;\n            }\n            if (img.dataset.mimetype !== \"image/svg+xml\") {\n                // Image data-mimetype should be changed to SVG since\n                // loadImageInfo() will set the original attachment mimetype on\n                // it.\n                img.dataset.mimetype = \"image/svg+xml\";\n            }\n        }\n    },\n    /**\n     * @private\n     */\n    async _reapplyCurrentShape() {\n        const img = this._getImg();\n        if (img.dataset.shape) {\n            await this._loadShape(img.dataset.shape);\n            await this._applyShapeAndColors(true, (img.dataset.shapeColors && img.dataset.shapeColors.split(';')));\n            img.classList.add(\"o_modified_image_to_save\");\n        }\n    },\n    /**\n     * @override\n     */\n    _isImageProcessingWidget(widgetName, params) {\n        if (widgetName === 'shape_img_opt') {\n            return !isGif(this._getImageMimetype(this._getImg()));\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Flips the image shape (vertically or/and horizontally).\n     *\n     * @private\n     * @param {string} flipValue image shape flip value\n     */\n    async _setImgShapeFlip(flipValue) {\n        const imgEl = this._getImg();\n        const currentFlipValue = imgEl.dataset.shapeFlip || \"\";\n        const newFlipValue = currentFlipValue.includes(flipValue)\n            ? currentFlipValue.replace(flipValue, \"\")\n            : currentFlipValue + flipValue;\n        if (newFlipValue) {\n            imgEl.dataset.shapeFlip = newFlipValue === \"yx\" ? \"xy\" : newFlipValue;\n        } else {\n            delete imgEl.dataset.shapeFlip;\n        }\n        await this._applyShapeAndColors(true, imgEl.dataset.shapeColors?.split(\";\"));\n        imgEl.classList.add(\"o_modified_image_to_save\");\n    },\n    /**\n     * Rotates the image shape 90 degrees.\n     *\n     * @private\n     * @param {integer} rotation rotation value\n     */\n    async _setImgShapeRotate(rotation) {\n        const imgEl = this._getImg();\n        const currentRotateValue = parseInt(imgEl.dataset.shapeRotate) || 0;\n        const newRotateValue = (currentRotateValue + rotation + 360) % 360;\n        if (newRotateValue) {\n            imgEl.dataset.shapeRotate = newRotateValue;\n        } else {\n            delete imgEl.dataset.shapeRotate;\n        }\n        await this._applyShapeAndColors(true, imgEl.dataset.shapeColors?.split(\";\"));\n        imgEl.classList.add(\"o_modified_image_to_save\");\n    },\n    /**\n     * Checks if the shape is in the \"devices\" category.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isDeviceShape() {\n        const imgEl = this._getImg();\n        const shapeName = imgEl.dataset.shape;\n        if (!shapeName) {\n            return false;\n        }\n        const shapeCategory = imgEl.dataset.shape.split(\"/\")[1];\n        return shapeCategory === \"devices\";\n    },\n    /**\n     * Checks if the shape is transformable.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isTransformableShape() {\n        const shapeImgWidget = this._requestUserValueWidgets(\"shape_img_opt\")[0];\n        return (shapeImgWidget && !shapeImgWidget.getMethodsParams().noTransform) && !this._isDeviceShape();\n    },\n    /**\n     * Checks if the shape is in animated.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isAnimatedShape() {\n        const shapeImgWidget = this._requestUserValueWidgets(\"shape_img_opt\")[0];\n        return shapeImgWidget?.getMethodsParams().animated;\n    },\n    /**\n     * Checks if squaring of image is required before application of shape.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isCropRequired() {\n        const shapeImgWidget = this._requestUserValueWidgets(\"shape_img_opt\")[0];\n        return shapeImgWidget?.getMethodsParams().unstretch;\n    },\n    /**\n     * Checks if the shape can have a hover effect.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _canHaveHoverEffect() {\n        return !this._isDeviceShape() && !this._isAnimatedShape() && this._isImageSupportedForShapes();\n    },\n    /**\n     * Adds hover effect to the SVG.\n     *\n     * @private\n     * @param {HTMLElement} svgEl\n     * @param {HTMLImageElement} [img] img element\n     */\n    async _addImageShapeHoverEffect(svgEl, img) {\n        let rgba = null;\n        let rbg = null;\n        let opacity = null;\n        // Add the required parts for the hover effects to the SVG.\n        const hoverEffectName = img.dataset.hoverEffect;\n        if (!this.hoverEffectsSvg) {\n            this.hoverEffectsSvg = await this._getHoverEffects();\n        }\n        const hoverEffectEls = this.hoverEffectsSvg.querySelectorAll(`#${hoverEffectName} > *`);\n        hoverEffectEls.forEach(hoverEffectEl => {\n            svgEl.appendChild(hoverEffectEl.cloneNode(true));\n        });\n        // Modifies the svg according to the chosen hover effect and the value\n        // of the options.\n        const animateEl = svgEl.querySelector(\"animate\");\n        const animateTransformEls = svgEl.querySelectorAll(\"animateTransform\");\n        const animateElValues = animateEl?.getAttribute(\"values\");\n        let animateTransformElValues = animateTransformEls[0]?.getAttribute(\"values\");\n        if (img.dataset.hoverEffectColor) {\n            rgba = convertCSSColorToRgba(img.dataset.hoverEffectColor);\n            rbg = `rgb(${rgba.red},${rgba.green},${rgba.blue})`;\n            opacity = rgba.opacity / 100;\n            if (![\"outline\", \"image_mirror_blur\"].includes(hoverEffectName)) {\n                svgEl.querySelector('[fill=\"hover_effect_color\"]').setAttribute(\"fill\", rbg);\n                animateEl.setAttribute(\"values\", animateElValues.replace(\"hover_effect_opacity\", opacity));\n            }\n        }\n        switch (hoverEffectName) {\n            case \"outline\": {\n                svgEl.querySelector('[stroke=\"hover_effect_color\"]').setAttribute(\"stroke\", rbg);\n                svgEl.querySelector('[stroke-opacity=\"hover_effect_opacity\"]').setAttribute(\"stroke-opacity\", opacity);\n                // The stroke width needs to be multiplied by two because half\n                // of the stroke is invisible since it is centered on the path.\n                const strokeWidth = parseInt(img.dataset.hoverEffectStrokeWidth) * 2;\n                animateEl.setAttribute(\"values\", animateElValues.replace(\"hover_effect_stroke_width\", strokeWidth));\n                break;\n            }\n            case \"image_zoom_in\":\n            case \"image_zoom_out\":\n            case \"dolly_zoom\": {\n                const imageEl = svgEl.querySelector(\"image\");\n                const clipPathEl = svgEl.querySelector(\"#clip-path\");\n                imageEl.setAttribute(\"id\", \"shapeImage\");\n                // Modify the SVG so that the clip-path is not zoomed when the\n                // image is zoomed.\n                imageEl.setAttribute(\"style\", \"transform-origin: center; width: 100%; height: 100%\");\n                imageEl.setAttribute(\"preserveAspectRatio\", \"none\");\n                svgEl.setAttribute(\"viewBox\", \"0 0 1 1\");\n                svgEl.setAttribute(\"preserveAspectRatio\", \"none\");\n                clipPathEl.setAttribute(\"clipPathUnits\", \"userSpaceOnUse\");\n                const clipPathValue = imageEl.getAttribute(\"clip-path\");\n                imageEl.removeAttribute(\"clip-path\");\n                const gEl = document.createElementNS(\"http://www.w3.org/2000/svg\", \"g\");\n                gEl.setAttribute(\"clip-path\", clipPathValue);\n                imageEl.parentNode.replaceChild(gEl, imageEl);\n                gEl.appendChild(imageEl);\n                let zoomValue = 1.01 + parseInt(img.dataset.hoverEffectIntensity) / 200;\n                animateTransformEls[0].setAttribute(\"values\", animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue));\n                if (hoverEffectName === \"image_zoom_out\") {\n                    // Set zoom intensity for the image.\n                    const styleAttr = svgEl.querySelector(\"style\");\n                    styleAttr.textContent = styleAttr.textContent.replace(\"hover_effect_zoom\", zoomValue);\n                }\n                if (hoverEffectName === \"dolly_zoom\") {\n                    clipPathEl.setAttribute(\"style\", \"transform-origin: center;\");\n                    // Set zoom intensity for clip-path and overlay.\n                    zoomValue = 0.99 - parseInt(img.dataset.hoverEffectIntensity) / 2000;\n                    animateTransformEls.forEach((animateTransformEl, index) => {\n                        if (index > 0) {\n                            animateTransformElValues = animateTransformEl.getAttribute(\"values\");\n                            animateTransformEl.setAttribute(\"values\", animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue));\n                        }\n                    });\n                }\n                break;\n            }\n            case \"image_mirror_blur\": {\n                const imageEl = svgEl.querySelector(\"image\");\n                imageEl.setAttribute('id', 'shapeImage');\n                imageEl.setAttribute('style', 'transform-origin: center;');\n                const imageMirrorEl = imageEl.cloneNode();\n                imageMirrorEl.setAttribute(\"id\", 'shapeImageMirror');\n                imageMirrorEl.setAttribute(\"filter\", \"url(#blurFilter)\");\n                imageEl.insertAdjacentElement(\"beforebegin\", imageMirrorEl);\n                const zoomValue = 0.99 - parseInt(img.dataset.hoverEffectIntensity) / 200;\n                animateTransformEls[0].setAttribute(\"values\", animateTransformElValues.replace(\"hover_effect_zoom\", zoomValue));\n                break;\n            }\n        }\n    },\n    /**\n     * Gets the hover effects list.\n     *\n     * @private\n     * @returns {HTMLElement}\n     */\n    _getHoverEffects() {\n        const hoverEffectsURL = \"/website/static/src/svg/hover_effects.svg\";\n        return fetch(hoverEffectsURL)\n            .then(response => response.text())\n            .then(text => {\n                const parser = new DOMParser();\n                const xmlDoc = parser.parseFromString(text, \"text/xml\");\n                return xmlDoc.getElementsByTagName(\"svg\")[0];\n            });\n    },\n    /**\n     * Disables the hover effect on the image.\n     *\n     * @private\n     */\n    async _disableHoverEffect() {\n        const imgEl = this._getImg();\n        const shapeName = imgEl.dataset.shape?.split(\"/\")[2];\n        delete imgEl.dataset.hoverEffect;\n        delete imgEl.dataset.hoverEffectColor;\n        delete imgEl.dataset.hoverEffectStrokeWidth;\n        delete imgEl.dataset.hoverEffectIntensity;\n        await this._applyOptions();\n        // If \"Square\" shape, remove it, it doesn't make sense to keep it\n        // without hover effect.\n        if (shapeName === \"geo_square\") {\n            this._requestUserValueWidgets(\"remove_img_shape_opt\")[0].enable();\n        }\n    },\n    /**\n     * @override\n     */\n    async _select(previewMode, widget) {\n        await this._super(...arguments);\n        // This is a special case where we need to override the \"_select\"\n        // function in order to trigger mouse events for hover effects on the\n        // images when previewing the options. This is done here because if it\n        // was done in one of the widget methods, the animation would be\n        // canceled when \"_refreshPublicWidgets\" is executed in the \"_super\"\n        if (widget.$el[0].closest(\"#o_hover_effects_options\")) {\n            const hasSetImgShapeHoverEffectMethod = widget.getMethodsNames().includes(\"setImgShapeHoverEffect\");\n            // We trigger the animation when preview mode is \"false\", except for\n            // the \"setImgShapeHoverEffect\" option, where we trigger it when\n            // preview mode is \"true\".\n            if (previewMode === hasSetImgShapeHoverEffectMethod) {\n                this.$target[0].dispatchEvent(new Event(\"mouseover\"));\n                this.hoverTimeoutId = setTimeout(() => {\n                    this.$target[0].dispatchEvent(new Event(\"mouseout\"));\n                }, 700);\n            } else if (previewMode === \"reset\") {\n                clearTimeout(this.hoverTimeoutId);\n            }\n        }\n    },\n    /**\n     * Checks if a shape can be applied on the target.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isImageSupportedForShapes() {\n        const imgEl = this._getImg();\n        return imgEl.dataset.originalId && this._isImageSupportedForProcessing(imgEl);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Reloads image data and auto-optimizes the new image.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onImageChanged(ev) {\n        this.trigger_up('snippet_edition_request', {exec: async () => {\n            await this._autoOptimizeImage();\n            this.trigger_up('cover_update');\n        }});\n    },\n    /**\n     * Available widths will change, need to rerender the width select.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onImageCropped(ev) {\n        await this._rerenderXML();\n    },\n});\n\n/**\n * Controls background image width and quality.\n */\nregistry.BackgroundOptimize = ImageHandlerOption.extend({\n    /**\n     * @override\n     */\n    start() {\n        this.$target.on('background_changed.BackgroundOptimize', this._onBackgroundChanged.bind(this));\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$target.off('.BackgroundOptimize');\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getImg() {\n        return this.img;\n    },\n    /**\n     * @override\n     */\n    _computeMaxDisplayWidth() {\n        return 1920;\n    },\n    /**\n     * Initializes this.img to an image with the background image url as src.\n     *\n     * @override\n     */\n    async _loadImageInfo() {\n        this.img = new Image();\n        // In the case of a parallax, the background of the snippet is actually\n        // set on a child <span> and should be focused here. This is necessary\n        // because, at this point, the $target has not yet been updated in the\n        // notify() method (\"option_update\" event), although the event is\n        // properly fired from the parallax.\n        const targetEl = this.$target[0].classList.contains(\"oe_img_bg\")\n            ? this.$target[0] : this.$target[0].querySelector(\":scope > .s_parallax_bg.oe_img_bg\");\n        if (targetEl) {\n            Object.entries(targetEl.dataset).filter(([key]) =>\n                isBackgroundImageAttribute(key)).forEach(([key, value]) => {\n                this.img.dataset[key] = value;\n            });\n            const src = getBgImageURL(targetEl);\n            // Don't set the src if not relative (ie, not local image: cannot be\n            // modified)\n            this.img.src = src.startsWith(\"/\") ? src : \"\";\n        }\n        return await this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _relocateWeightEl() {\n        this.trigger_up('option_update', {\n            optionNames: ['BackgroundImage'],\n            name: 'add_size_indicator',\n            data: this.$weight,\n        });\n    },\n    /**\n     * @override\n     */\n    _applyImage(img) {\n        const parts = backgroundImageCssToParts(this.$target.css('background-image'));\n        parts.url = `url('${img.getAttribute('src')}')`;\n        const combined = backgroundImagePartsToCss(parts);\n        this.$target.css('background-image', combined);\n        // Apply modification on the DOM HTML element that is currently being\n        // modified.\n        this.$target[0].classList.add(\"o_modified_image_to_save\");\n        // First delete the data attributes relative to the image background\n        // from the target as a data attribute could have been be removed (ex:\n        // glFilter).\n        for (const attribute in this.$target[0].dataset) {\n            if (isBackgroundImageAttribute(attribute)) {\n                delete this.$target[0].dataset[attribute];\n            }\n        }\n        Object.entries(img.dataset).forEach(([key, value]) => {\n            this.$target[0].dataset[key] = value;\n        });\n        this.$target[0].dataset.bgSrc = img.getAttribute(\"src\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Reloads image data when the background is changed.\n     *\n     * @private\n     */\n    async _onBackgroundChanged(ev, previewMode) {\n        ev.stopPropagation();\n        if (!previewMode) {\n            this.trigger_up('snippet_edition_request', {exec: async () => {\n                await this._autoOptimizeImage();\n            }});\n        }\n    },\n});\n\nregistry.BackgroundToggler = SnippetOptionWidget.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Toggles background image on or off.\n     *\n     * @see this.selectClass for parameters\n     */\n    toggleBgImage(previewMode, widgetValue, params) {\n        if (!widgetValue) {\n            this.$target.find('> .o_we_bg_filter').remove();\n            // TODO: use setWidgetValue instead of calling background directly when possible\n            const [bgImageWidget] = this._requestUserValueWidgets('bg_image_opt');\n            const bgImageOpt = bgImageWidget.getParent();\n            return bgImageOpt.background(false, '', bgImageWidget.getMethodsParams('background'));\n        } else {\n            // TODO: use trigger instead of el.click when possible\n            this._requestUserValueWidgets('bg_image_opt')[0].el.click();\n        }\n    },\n    /**\n     * Toggles background shape on or off.\n     *\n     * @see this.selectClass for parameters\n     */\n    toggleBgShape(previewMode, widgetValue, params) {\n        const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');\n        const shapeOption = shapeWidget.getParent();\n        // TODO: open select after shape was selected?\n        // TODO: use setWidgetValue instead of calling shapeOption method directly when possible\n        return shapeOption._toggleShape();\n    },\n    /**\n     * Sets a color filter.\n     *\n     * @see this.selectClass for parameters\n     */\n    async selectFilterColor(previewMode, widgetValue, params) {\n        // Find the filter element.\n        let filterEl = this.$target[0].querySelector(':scope > .o_we_bg_filter');\n\n        // If the filter would be transparent, remove it / don't create it.\n        const rgba = widgetValue && convertCSSColorToRgba(widgetValue);\n        if (!widgetValue || rgba && rgba.opacity < 0.001) {\n            if (filterEl) {\n                filterEl.remove();\n            }\n            return;\n        }\n\n        // Create the filter if necessary.\n        if (!filterEl) {\n            filterEl = document.createElement('div');\n            filterEl.classList.add('o_we_bg_filter');\n            const lastBackgroundEl = this._getLastPreFilterLayerElement();\n            if (lastBackgroundEl) {\n                $(lastBackgroundEl).after(filterEl);\n            } else {\n                this.$target.prepend(filterEl);\n            }\n        }\n\n        // Apply the color on the filter.\n        const obj = createPropertyProxy(this, '$target', $(filterEl));\n        params.cssProperty = 'background-color';\n        return this.selectStyle.call(obj, previewMode, widgetValue, params);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'toggleBgImage': {\n                const [bgImageWidget] = this._requestUserValueWidgets('bg_image_opt');\n                const bgImageOpt = bgImageWidget.getParent();\n                return !!bgImageOpt._computeWidgetState('background', bgImageWidget.getMethodsParams('background'));\n            }\n            case 'toggleBgShape': {\n                const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');\n                const shapeOption = shapeWidget.getParent();\n                return !!shapeOption._computeWidgetState('shape', shapeWidget.getMethodsParams('shape'));\n            }\n            case 'selectFilterColor': {\n                const filterEl = this.$target[0].querySelector(':scope > .o_we_bg_filter');\n                if (!filterEl) {\n                    return '';\n                }\n                const obj = createPropertyProxy(this, '$target', $(filterEl));\n                params.cssProperty = 'background-color';\n                return this._computeWidgetState.call(obj, 'selectStyle', params);\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _getLastPreFilterLayerElement() {\n        return null;\n    },\n});\n\n/**\n * Handles the edition of snippet's background image.\n */\nregistry.BackgroundImage = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this.__customImageSrc = getBgImageURL(this.$target[0]);\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles a background change.\n     *\n     * @see this.selectClass for parameters\n     */\n    background: async function (previewMode, widgetValue, params) {\n        if (previewMode === true) {\n            this.__customImageSrc = getBgImageURL(this.$target[0]);\n        } else if (previewMode === 'reset') {\n            widgetValue = this.__customImageSrc;\n        } else {\n            this.__customImageSrc = widgetValue;\n        }\n\n        this._setBackground(widgetValue);\n\n        if (previewMode !== 'reset') {\n            removeOnImageChangeAttrs.forEach(attr => delete this.$target[0].dataset[attr]);\n            this.$target.trigger('background_changed', [previewMode]);\n        }\n    },\n    /**\n     * Changes the main color of dynamic SVGs.\n     *\n     * @see this.selectClass for parameters\n     */\n    async dynamicColor(previewMode, widgetValue, params) {\n        const currentSrc = getBgImageURL(this.$target[0]);\n        switch (previewMode) {\n            case true:\n                this.previousSrc = currentSrc;\n                break;\n            case 'reset':\n                this._setBackground(this.previousSrc);\n                return;\n        }\n        const newURL = new URL(currentSrc, window.location.origin);\n        newURL.searchParams.set(params.colorName, normalizeColor(widgetValue));\n        const src = newURL.pathname + newURL.search;\n        await loadImage(src);\n        this._setBackground(src);\n        if (!previewMode) {\n            this.previousSrc = src;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    notify(name, data) {\n        if (name === 'add_size_indicator') {\n            this._requestUserValueWidgets('bg_image_opt')[0].$el.after(data);\n        } else {\n            this._super(...arguments);\n        }\n    },\n    /**\n     * @override\n     */\n    setTarget: function () {\n        // When we change the target of this option we need to transfer the\n        // background-image and the dataset information relative to this image\n        // from the old target to the new one.\n        const oldBgURL = getBgImageURL(this.$target);\n        const isModifiedImage = this.$target[0].classList.contains(\"o_modified_image_to_save\");\n        const filteredOldDataset = Object.entries(this.$target[0].dataset).filter(([key]) => {\n            return isBackgroundImageAttribute(key);\n        });\n        // Delete the dataset information relative to the background-image of\n        // the old target.\n        filteredOldDataset.forEach(([key]) => {\n            delete this.$target[0].dataset[key];\n        });\n        // It is important to delete \".o_modified_image_to_save\" from the old\n        // target as its image source will be deleted.\n        this.$target[0].classList.remove(\"o_modified_image_to_save\");\n        this._setBackground('');\n        this._super(...arguments);\n        if (oldBgURL) {\n            this._setBackground(oldBgURL);\n            filteredOldDataset.forEach(([key, value]) => {\n                this.$target[0].dataset[key] = value;\n            });\n            this.$target[0].classList.toggle(\"o_modified_image_to_save\", isModifiedImage);\n        }\n\n        // TODO should be automatic for all options as equal to the start method\n        this.__customImageSrc = getBgImageURL(this.$target[0]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'background':\n                return getBgImageURL(this.$target[0]);\n            case 'dynamicColor':\n                return new URL(getBgImageURL(this.$target[0]), window.location.origin).searchParams.get(params.colorName);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if ('colorName' in params) {\n            const src = new URL(getBgImageURL(this.$target[0]), window.location.origin);\n            return src.searchParams.has(params.colorName);\n        } else if (widgetName === 'main_color_opt') {\n            const src = new URL(getBgImageURL(this.$target[0]), window.location.origin);\n            return src.origin === window.location.origin && (\n                src.pathname.startsWith('/html_editor/shape/') ||\n                src.pathname.startsWith('/web_editor/shape/')\n            );\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     * @param {string} backgroundURL\n     */\n    _setBackground(backgroundURL) {\n        const parts = backgroundImageCssToParts(this.$target.css('background-image'));\n        if (backgroundURL) {\n            parts.url = `url('${backgroundURL}')`;\n            this.$target.addClass('oe_img_bg o_bg_img_center');\n        } else {\n            delete parts.url;\n            this.$target[0].classList.remove(\n                \"oe_img_bg\",\n                \"o_bg_img_center\",\n                \"o_modified_image_to_save\",\n            );\n        }\n        const combined = backgroundImagePartsToCss(parts);\n        // We use selectStyle so that if when a background image is removed the\n        // remaining image matches the o_cc's gradient background, it can be\n        // removed too.\n        this.selectStyle(false, combined, {\n            cssProperty: 'background-image',\n        });\n        this.options.wysiwyg.odooEditor.editable.focus();\n    },\n});\n\n/**\n * Handles background shapes.\n */\nregistry.BackgroundShape = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    updateUI({assetsChanged} = {}) {\n        if (this.rerender || assetsChanged) {\n            this.rerender = false;\n            return this._rerenderXML();\n        }\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    onBuilt() {\n        // Flip classes should no longer be used but are still present in some\n        // theme snippets.\n        if (this.$target[0].querySelector('.o_we_flip_x, .o_we_flip_y')) {\n            this._handlePreviewState(false, () => {\n                return {flip: this._getShapeData().flip};\n            });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets the current background shape.\n     *\n     * @see this.selectClass for params\n     */\n    shape(previewMode, widgetValue, params) {\n        this._handlePreviewState(previewMode, () => {\n            return {\n                shape: widgetValue,\n                colors: this._getImplicitColors(widgetValue, this._getShapeData().colors),\n                flip: [],\n                animated: params.animated,\n                shapeAnimationSpeed: this._getShapeData().shapeAnimationSpeed,\n            };\n        });\n    },\n    /**\n     * Sets the current background shape's colors.\n     *\n     * @see this.selectClass for params\n     */\n    color(previewMode, widgetValue, params) {\n        this._handlePreviewState(previewMode, () => {\n            const {colorName} = params;\n            const {colors: previousColors} = this._getShapeData();\n            const newColor = normalizeColor(widgetValue) || this._getDefaultColors()[colorName];\n            const newColors = Object.assign(previousColors, {[colorName]: newColor});\n            return {colors: newColors};\n        });\n    },\n    /**\n     * Flips the shape on its x axis.\n     *\n     * @see this.selectClass for params\n     */\n    flipX(previewMode, widgetValue, params) {\n        this._flipShape(previewMode, 'x');\n    },\n    /**\n     * Flips the shape on its y axis.\n     *\n     * @see this.selectClass for params\n     */\n    flipY(previewMode, widgetValue, params) {\n        this._flipShape(previewMode, 'y');\n    },\n    /**\n     * Shows/Hides the shape on mobile.\n     *\n     * @see this.selectClass for params\n     */\n    showOnMobile(previewMode, widgetValue, params) {\n        this._handlePreviewState(previewMode, () => {\n            return {showOnMobile: !this._getShapeData().showOnMobile};\n        });\n    },\n    /**\n     * Sets the speed of the animation of a background shape.\n     *\n     * @see this.selectClass for params\n     */\n    setBgShapeAnimationSpeed(previewMode, widgetValue, params) {\n        this._handlePreviewState(previewMode, () => {\n            return { shapeAnimationSpeed: widgetValue };\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'shape': {\n                return this._getShapeData().shape;\n            }\n            case 'color': {\n                const {shape, colors: customColors} = this._getShapeData();\n                const colors = Object.assign(this._getDefaultColors(), customColors);\n                const color = shape && colors[params.colorName];\n                return color || '';\n            }\n            case 'flipX': {\n                // Compat: flip classes are no longer used but may be present in client db\n                const hasFlipClass = this.$target.find('> .o_we_shape.o_we_flip_x').length !== 0;\n                return hasFlipClass || this._getShapeData().flip.includes('x');\n            }\n            case 'flipY': {\n                // Compat: flip classes are no longer used but may be present in client db\n                const hasFlipClass = this.$target.find('> .o_we_shape.o_we_flip_y').length !== 0;\n                return hasFlipClass || this._getShapeData().flip.includes('y');\n            }\n            case 'showOnMobile': {\n                return this._getShapeData().showOnMobile;\n            }\n            case \"setBgShapeAnimationSpeed\": {\n                return this._getShapeData().shapeAnimationSpeed;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === \"bg_shape_anim_speed_opt\") {\n            const bgShapeWidget = this._requestUserValueWidgets(\"bg_shape_opt\")[0];\n            return bgShapeWidget.getMethodsParams().animated === \"true\";\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        Object.keys(this._getDefaultColors()).map(colorName => {\n            uiFragment.querySelector('[data-name=\"colors\"]')\n                .prepend($(`<we-colorpicker data-color=\"true\" data-color-name=\"${colorName}\"></we-colorpicker>`)[0]);\n        });\n\n        // Inventory shape URLs per class.\n        const style = window.getComputedStyle(this.$target[0]);\n        const palette = [1, 2, 3, 4, 5].map(n => style.getPropertyValue(`--o-cc${n}-bg`)).join();\n        if (palette !== this._lastShapePalette) {\n            this._lastShapePalette = palette;\n            this._shapeBackgroundImagePerClass = {};\n            for (const styleSheet of this.$target[0].ownerDocument.styleSheets) {\n                if (styleSheet.href && new URL(styleSheet.href).host !== location.host) {\n                    // In some browsers, if a stylesheet is loaded from a different domain\n                    // accessing cssRules results in a SecurityError.\n                    continue;\n                }\n                for (const rule of [...styleSheet.cssRules]) {\n                    if (rule.selectorText && rule.selectorText.startsWith(\".o_we_shape.\")) {\n                        this._shapeBackgroundImagePerClass[rule.selectorText] = rule.style.backgroundImage;\n                    }\n                }\n            }\n        }\n\n        uiFragment.querySelectorAll('we-select-pager we-button[data-shape]').forEach(btn => {\n            const btnContent = document.createElement('div');\n            btnContent.classList.add('o_we_shape_btn_content', 'position-relative', 'border-dark');\n            const btnContentInnerDiv = document.createElement('div');\n            btnContentInnerDiv.classList.add('o_we_shape');\n            btnContent.appendChild(btnContentInnerDiv);\n\n            if (btn.dataset.animated) {\n                _addAnimatedShapeLabel(btnContent);\n            }\n\n            const {shape} = btn.dataset;\n            const shapeEl = btnContent.querySelector('.o_we_shape');\n            const shapeClassName = `o_${shape.replace(/\\//g, '_')}`;\n            shapeEl.classList.add(shapeClassName);\n            // Match current palette.\n            const shapeBackgroundImage = this._shapeBackgroundImagePerClass[`.o_we_shape.${shapeClassName}`];\n            shapeEl.style.setProperty(\"background-image\", shapeBackgroundImage);\n            btn.append(btnContent);\n        });\n        return uiFragment;\n    },\n    /**\n     * Flips the shape on its x/y axis.\n     *\n     * @param {boolean} previewMode\n     * @param {'x'|'y'} axis the axis of the shape that should be flipped.\n     */\n    _flipShape(previewMode, axis) {\n        this._handlePreviewState(previewMode, () => {\n            const flip = new Set(this._getShapeData().flip);\n            if (flip.has(axis)) {\n                flip.delete(axis);\n            } else {\n                flip.add(axis);\n            }\n            return {flip: [...flip]};\n        });\n    },\n    /**\n     * Inserts or removes the given container at the right position in the\n     * document.\n     *\n     * @param {HTMLElement} [newContainer] container to insert, null to remove\n     */\n    _insertShapeContainer(newContainer) {\n        const target = this.$target[0];\n\n        const shapeContainer = target.querySelector(':scope > .o_we_shape');\n        if (shapeContainer) {\n            this._removeShapeEl(shapeContainer);\n        }\n        if (newContainer) {\n            const preShapeLayerElement = this._getLastPreShapeLayerElement();\n            if (preShapeLayerElement) {\n                $(preShapeLayerElement).after(newContainer);\n            } else {\n                this.$target.prepend(newContainer);\n            }\n        }\n        return newContainer;\n    },\n    /**\n     * Creates and inserts a container for the shape with the right classes.\n     *\n     * @param {string} shape the shape name for which to create a container\n     */\n    _createShapeContainer(shape) {\n        const shapeContainer = this._insertShapeContainer(document.createElement('div'));\n        this.$target[0].style.position = 'relative';\n        shapeContainer.className = `o_we_shape o_${shape.replace(/\\//g, '_')}`;\n        return shapeContainer;\n    },\n    /**\n     * Handles everything related to saving state before preview and restoring\n     * it after a preview or locking in the changes when not in preview.\n     *\n     * @param {boolean} previewMode\n     * @param {function} computeShapeData function to compute the new shape data.\n     */\n    _handlePreviewState(previewMode, computeShapeData) {\n        const target = this.$target[0];\n\n        let changedShape = false;\n        if (previewMode === 'reset') {\n            this._insertShapeContainer(this.prevShapeContainer);\n            if (this.prevShape) {\n                target.dataset.oeShapeData = this.prevShape;\n            } else {\n                delete target.dataset.oeShapeData;\n            }\n            return;\n        } else {\n            if (previewMode === true) {\n                const shapeContainer = target.querySelector(':scope > .o_we_shape');\n                this.prevShapeContainer = shapeContainer && shapeContainer.cloneNode(true);\n                this.prevShape = target.dataset.oeShapeData;\n            }\n            const curShapeData = target.dataset.oeShapeData || {};\n            const newShapeData = computeShapeData();\n            const {shape: curShape} = curShapeData;\n            changedShape = newShapeData.shape !== curShape;\n            this._markShape(newShapeData);\n            if (previewMode === false && changedShape) {\n                // Need to rerender for correct number of colorpickers\n                this.rerender = true;\n            }\n        }\n\n        // Updates/removes the shape container as needed and gives it the\n        // correct background shape\n        const json = target.dataset.oeShapeData;\n        const {shape, colors, flip = [], animated = 'false', showOnMobile, shapeAnimationSpeed} = json ? JSON.parse(json) : {};\n        let shapeContainer = target.querySelector(':scope > .o_we_shape');\n        if (!shape) {\n            return this._insertShapeContainer(null);\n        }\n        // When changing shape we want to reset the shape container (for transparency color)\n        if (changedShape) {\n            shapeContainer = this._createShapeContainer(shape);\n        }\n        // Compat: remove old flip classes as flipping is now done inside the svg\n        shapeContainer.classList.remove('o_we_flip_x', 'o_we_flip_y');\n\n        shapeContainer.classList.toggle('o_we_animated', animated === 'true');\n        if (colors || flip.length || parseFloat(shapeAnimationSpeed) !== 0) {\n            // Custom colors/flip/speed, overwrite shape that is set by the class\n            $(shapeContainer).css('background-image', `url(\"${this._getShapeSrc()}\")`);\n            shapeContainer.style.backgroundPosition = '';\n            if (flip.length) {\n                let [xPos, yPos] = $(shapeContainer)\n                    .css('background-position')\n                    .split(' ')\n                    .map(p => parseFloat(p));\n                // -X + 2*Y is a symmetry of X around Y, this is a symmetry around 50%\n                xPos = flip.includes('x') ? -xPos + 100 : xPos;\n                yPos = flip.includes('y') ? -yPos + 100 : yPos;\n                shapeContainer.style.backgroundPosition = `${xPos}% ${yPos}%`;\n            }\n        } else {\n            // Remove custom bg image and let the shape class set the bg shape\n            $(shapeContainer).css('background-image', '');\n            $(shapeContainer).css('background-position', '');\n        }\n        shapeContainer.classList.toggle('o_shape_show_mobile', !!showOnMobile);\n        if (previewMode === false) {\n            this.prevShapeContainer = shapeContainer.cloneNode(true);\n            this.prevShape = target.dataset.oeShapeData;\n        }\n    },\n    /**\n     * @private\n     * @param {HTMLElement} shapeEl\n     */\n    _removeShapeEl(shapeEl) {\n        shapeEl.remove();\n    },\n    /**\n     * Overwrites shape properties with the specified data.\n     *\n     * @private\n     * @param {Object} newData an object with the new data\n     */\n    _markShape(newData) {\n        const defaultColors = this._getDefaultColors();\n        const shapeData = Object.assign(this._getShapeData(), newData);\n        const areColorsDefault = Object.entries(shapeData.colors).every(([colorName, colorValue]) => {\n            return defaultColors[colorName] && colorValue.toLowerCase() === defaultColors[colorName].toLowerCase();\n        });\n        if (areColorsDefault) {\n            delete shapeData.colors;\n        }\n        if (!shapeData.shape) {\n            delete this.$target[0].dataset.oeShapeData;\n        } else {\n            this.$target[0].dataset.oeShapeData = JSON.stringify(shapeData);\n        }\n    },\n    /**\n     * @private\n     */\n    _getLastPreShapeLayerElement() {\n        const $filterEl = this.$target.find('> .o_we_bg_filter');\n        if ($filterEl.length) {\n            return $filterEl[0];\n        }\n        return null;\n    },\n    /**\n     * Returns the src of the shape corresponding to the current parameters.\n     *\n     * @private\n     */\n    _getShapeSrc() {\n        const { shape, colors, flip, shapeAnimationSpeed } = this._getShapeData();\n        if (!shape) {\n            return '';\n        }\n        const searchParams = Object.entries(colors)\n            .map(([colorName, colorValue]) => {\n                const encodedCol = encodeURIComponent(colorValue);\n                return `${colorName}=${encodedCol}`;\n            });\n        if (flip.length) {\n            searchParams.push(`flip=${encodeURIComponent(flip.sort().join(''))}`);\n        }\n        if (Number(shapeAnimationSpeed)) {\n            searchParams.push(`shapeAnimationSpeed=${encodeURIComponent(shapeAnimationSpeed)}`);\n        }\n        return `/web_editor/shape/${encodeURIComponent(shape)}.svg?${searchParams.join('&')}`;\n    },\n    /**\n     * Retrieves current shape data from the target's dataset.\n     *\n     * @private\n     * @param {HTMLElement} [target=this.$target[0]] the target on which to read\n     *   the shape data.\n     */\n    _getShapeData(target = this.$target[0]) {\n        const defaultData = {\n            shape: '',\n            colors: this._getDefaultColors($(target)),\n            flip: [],\n            showOnMobile: false,\n            shapeAnimationSpeed: \"0\",\n        };\n        const json = target.dataset.oeShapeData;\n        return json ? Object.assign(defaultData, JSON.parse(json.replace(/'/g, '\"'))) : defaultData;\n    },\n    /**\n     * Returns the default colors for the currently selected shape.\n     *\n     * @private\n     * @param {jQueryElement} [$target=this.$target] the target on which to read\n     *   the shape data.\n     */\n    _getDefaultColors($target = this.$target) {\n        const $shapeContainer = $target.find('> .o_we_shape')\n            .clone()\n            .addClass('d-none')\n            // Needs to be in document for bg-image class to take effect\n            .appendTo(this.$target[0].ownerDocument.body);\n        const shapeContainer = $shapeContainer[0];\n        $shapeContainer.css('background-image', '');\n        const shapeSrc = shapeContainer && getBgImageURL(shapeContainer);\n        $shapeContainer.remove();\n        if (!shapeSrc) {\n            return {};\n        }\n        const url = new URL(shapeSrc, window.location.origin);\n        return Object.fromEntries(url.searchParams.entries());\n    },\n    /**\n     * Returns the default colors for the a shape in the selector.\n     *\n     * @private\n     * @param {String} shapeId identifier of the shape\n     */\n    _getShapeDefaultColors(shapeId) {\n        const $shapeContainer = this.$el.find(\".o_we_bg_shape_menu we-button[data-shape='\" + shapeId + \"'] div.o_we_shape\");\n        const shapeContainer = $shapeContainer[0];\n        const shapeSrc = shapeContainer && getBgImageURL(shapeContainer);\n        const url = new URL(shapeSrc, window.location.origin);\n        return Object.fromEntries(url.searchParams.entries());\n    },\n    /**\n     * Returns the implicit colors for the currently selected shape.\n     *\n     * The implicit colors are use upon shape selection. They are computed as:\n     * - the default colors\n     * - patched with each set of colors of previous siblings shape\n     * - patched with the colors of the previously selected shape\n     * - filtered to only keep the colors involved in the current shape\n     *\n     * @private\n     * @param {String} shape identifier of the selected shape\n     * @param {Object} previousColors colors of the shape before its replacement\n     */\n    _getImplicitColors(shape, previousColors) {\n        const defaultColors = this._getShapeDefaultColors(shape);\n        let colors = previousColors || {};\n        let sibling = this.$target[0].previousElementSibling;\n        while (sibling) {\n            colors = Object.assign(this._getShapeData(sibling).colors || {}, colors);\n            sibling = sibling.previousElementSibling;\n        }\n        const defaultKeys = Object.keys(defaultColors);\n        colors = Object.assign(defaultColors, colors);\n        return pick(colors, ...defaultKeys);\n    },\n    /**\n     * Toggles whether there is a shape or not, to be called from bg toggler.\n     *\n     * @private\n     */\n    _toggleShape() {\n        if (this._getShapeData().shape) {\n            return this._handlePreviewState(false, () => ({shape: ''}));\n        } else {\n            const target = this.$target[0];\n            const previousSibling = target.previousElementSibling;\n            const [shapeWidget] = this._requestUserValueWidgets('bg_shape_opt');\n            const possibleShapes = shapeWidget.getMethodsParams('shape').possibleValues;\n            let shapeToSelect;\n            if (previousSibling) {\n                const previousShape = this._getShapeData(previousSibling).shape;\n                shapeToSelect = possibleShapes.find((shape, i) => {\n                    return possibleShapes[i - 1] === previousShape;\n                });\n            }\n            // If there is no previous sibling, if the previous sibling had the\n            // last shape selected or if the previous shape could not be found\n            // in the possible shapes, default to the first shape. ([0] being no\n            // shapes selected.)\n            if (!shapeToSelect) {\n                shapeToSelect = possibleShapes[1];\n            }\n            // Only show on mobile by default if toggled from mobile view\n            const showOnMobile = weUtils.isMobileView(this.$target[0]);\n            this.trigger_up('snippet_edition_request', {exec: () => {\n                // options for shape will only be available after _toggleShape() returned\n                this._requestUserValueWidgets('bg_shape_opt')[0].enable();\n            }});\n            this._createShapeContainer(shapeToSelect);\n            return this._handlePreviewState(false, () => (\n                {\n                    shape: shapeToSelect,\n                    colors: this._getImplicitColors(shapeToSelect),\n                    showOnMobile,\n                }\n            ));\n        }\n    },\n});\n\n/**\n * Handles the edition of snippets' background image position.\n */\nregistry.BackgroundPosition = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this._super.apply(this, arguments);\n\n        this._initOverlay();\n\n        // Resize overlay content on window resize because background images\n        // change size, and on carousel slide because they sometimes take up\n        // more space and move elements around them.\n        $(window).on('resize.bgposition', () => this._dimensionOverlay());\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._toggleBgOverlay(false);\n        $(window).off('.bgposition');\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets the background type (cover/repeat pattern).\n     *\n     * @see this.selectClass for params\n     */\n    backgroundType: function (previewMode, widgetValue, params) {\n        this.$target.toggleClass('o_bg_img_opt_repeat', widgetValue === 'repeat-pattern');\n        this.$target.css('background-position', '');\n        this.$target.css('background-size', widgetValue !== 'repeat-pattern' ? '' : '100px');\n    },\n    /**\n     * Saves current background position and enables overlay.\n     *\n     * @see this.selectClass for params\n     */\n    backgroundPositionOverlay: async function (previewMode, widgetValue, params) {\n        // Updates the internal image\n        await new Promise(resolve => {\n            this.img = document.createElement('img');\n            this.img.addEventListener('load', () => resolve());\n            this.img.src = getBgImageURL(this.$target[0]);\n        });\n\n        const position = this.$target.css('background-position').split(' ').map(v => parseInt(v));\n        const delta = this._getBackgroundDelta();\n        // originalPosition kept in % for when movement in one direction doesn't make sense\n        this.originalPosition = {\n            left: position[0],\n            top: position[1],\n        };\n        // Convert % values to pixels for current position because mouse movement is in pixels\n        this.currentPosition = {\n            left: position[0] / 100 * delta.x || 0,\n            top: position[1] / 100 * delta.y || 0,\n        };\n        // Make sure the element is in a visible area.\n        const rect = this.$target[0].getBoundingClientRect();\n        const viewportTop = $(window).scrollTop();\n        const viewportBottom = viewportTop + $(window).height();\n        const visibleHeight = rect.top < viewportTop\n            ? Math.max(0, Math.min(viewportBottom, rect.bottom) - viewportTop) // Starts above\n            : rect.top < viewportBottom\n                ? Math.min(viewportBottom, rect.bottom) - rect.top // Starts inside\n                : 0; // Starts after\n        if (visibleHeight < 200) {\n            await scrollTo(this.$target[0], { extraOffset: 50 });\n        }\n        this._toggleBgOverlay(true);\n    },\n    /**\n     * @override\n     */\n    selectStyle: function (previewMode, widgetValue, params) {\n        if (params.cssProperty === 'background-size'\n                && !this.$target.hasClass('o_bg_img_opt_repeat')) {\n            // Disable the option when the image is in cover mode, otherwise\n            // the background-size: auto style may be forced.\n            return;\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeVisibility: function () {\n        return this._super(...arguments) && !!getBgImageURL(this.$target[0]);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'backgroundType') {\n            return this.$target.css('background-repeat') === 'repeat' ? 'repeat-pattern' : 'cover';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Initializes the overlay, binds events to the buttons, inserts it in\n     * the DOM.\n     *\n     * @private\n     */\n    _initOverlay: function () {\n        this.$backgroundOverlay = $(renderToElement('web_editor.background_position_overlay'));\n        this.$overlayContent = this.$backgroundOverlay.find('.o_we_overlay_content');\n        this.$overlayBackground = this.$overlayContent.find('.o_overlay_background');\n\n        this.$backgroundOverlay.on('click', '.o_btn_apply', () => {\n            this.$target.css('background-position', this.$bgDragger.css('background-position'));\n            this._toggleBgOverlay(false);\n        });\n        this.$backgroundOverlay.on('click', '.o_btn_discard', () => {\n            this._toggleBgOverlay(false);\n        });\n\n        this.$backgroundOverlay.insertAfter(this.$overlay);\n    },\n    /**\n     * Sets the overlay in the right place so that the draggable background\n     * renders over the target, and size the background item like the target.\n     *\n     * @private\n     */\n    _dimensionOverlay: function () {\n        if (!this.$backgroundOverlay.is('.oe_active')) {\n            return;\n        }\n        // TODO: change #wrapwrap after web_editor rework.\n        const $wrapwrap = $(this.ownerDocument.body).find(\"#wrapwrap\");\n        const targetOffset = this.$target.offset();\n\n        this.$backgroundOverlay.css({\n            width: $wrapwrap.innerWidth(),\n            height: $wrapwrap.innerHeight(),\n        });\n\n        this.$overlayContent.offset(targetOffset);\n\n        this.$bgDragger.css({\n            width: `${this.$target.innerWidth()}px`,\n            height: `${this.$target.innerHeight()}px`,\n        });\n\n        const topPos = Math.max(0, $(window).scrollTop() - this.$target.offset().top);\n        this.$overlayContent.find('.o_we_overlay_buttons').css('top', `${topPos}px`);\n    },\n    /**\n     * Toggles the overlay's display and renders a background clone inside of it.\n     *\n     * @private\n     * @param {boolean} activate toggle the overlay on (true) or off (false)\n     */\n    _toggleBgOverlay: function (activate) {\n        if (!this.$backgroundOverlay || this.$backgroundOverlay.is('.oe_active') === activate) {\n            return;\n        }\n\n        if (!activate) {\n            this.$backgroundOverlay.removeClass('oe_active');\n            this.trigger_up('unblock_preview_overlays');\n            this.trigger_up('activate_snippet', {$snippet: this.$target});\n\n            $(document).off('click.bgposition');\n            if (this.$bgDragger) {\n                this.$bgDragger.tooltip('dispose');\n            }\n            return;\n        }\n\n        this.trigger_up('hide_overlay');\n        this.trigger_up('activate_snippet', {\n            $snippet: this.$target,\n            previewMode: true,\n        });\n        this.trigger_up('block_preview_overlays');\n\n        // Create empty clone of $target with same display size, make it draggable and give it a tooltip.\n        this.$bgDragger = this.$target.clone().empty();\n        // Prevent clone from being seen as editor if target is editor (eg. background on root tag)\n        this.$bgDragger.removeClass('o_editable');\n        // Some CSS child selector rules will not be applied since the clone has a different container from $target.\n        // The background-attachment property should be the same in both $target & $bgDragger, this will keep the\n        // preview more \"wysiwyg\" instead of getting different result when bg position saved (e.g. parallax snippet)\n        // TODO: improve this to copy all style from $target and override it with overlay related style (copying all\n        // css into $bgDragger will not work since it will change overlay content style too).\n        this.$bgDragger.css('background-attachment', this.$target.css('background-attachment'));\n        this.$bgDragger.on('mousedown', this._onDragBackgroundStart.bind(this));\n        this.$bgDragger.tooltip({\n            title: 'Click and drag the background to adjust its position!',\n            trigger: 'manual',\n            container: this.$backgroundOverlay\n        });\n\n        // Replace content of overlayBackground, activate the overlay and give it the right dimensions.\n        this.$overlayBackground.empty().append(this.$bgDragger);\n        this.$backgroundOverlay.addClass('oe_active');\n        this._dimensionOverlay();\n        this.$bgDragger.tooltip('show');\n\n        // Needs to be deferred or the click event that activated the overlay deactivates it as well.\n        // This is caused by the click event which we are currently handling bubbling up to the document.\n        window.setTimeout(() => $(document).on('click.bgposition', this._onDocumentClicked.bind(this)), 0);\n    },\n    /**\n     * Returns the difference between the target's size and the background's\n     * rendered size. Background position values in % are a percentage of this.\n     *\n     * @private\n     */\n    _getBackgroundDelta: function () {\n        const bgSize = this.$target.css('background-size');\n        if (bgSize !== 'cover') {\n            let [width, height] = bgSize.split(' ');\n            if (width === 'auto' && (height === 'auto' || !height)) {\n                return {\n                    x: this.$target.outerWidth() - this.img.naturalWidth,\n                    y: this.$target.outerHeight() - this.img.naturalHeight,\n                };\n            }\n            // At least one of width or height is not auto, so we can use it to calculate the other if it's not set\n            [width, height] = [parseInt(width), parseInt(height)];\n            return {\n                x: this.$target.outerWidth() - (width || (height * this.img.naturalWidth / this.img.naturalHeight)),\n                y: this.$target.outerHeight() - (height || (width * this.img.naturalHeight / this.img.naturalWidth)),\n            };\n        }\n\n        const renderRatio = Math.max(\n            this.$target.outerWidth() / this.img.naturalWidth,\n            this.$target.outerHeight() / this.img.naturalHeight\n        );\n\n        return {\n            x: this.$target.outerWidth() - Math.round(renderRatio * this.img.naturalWidth),\n            y: this.$target.outerHeight() - Math.round(renderRatio * this.img.naturalHeight),\n        };\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Drags the overlay's background image, copied to target on \"Apply\".\n     *\n     * @private\n     */\n    _onDragBackgroundStart: function (ev) {\n        ev.preventDefault();\n        this.$bgDragger.addClass('o_we_grabbing');\n        const $document = $(this.$target[0].ownerDocument);\n        $document.on('mousemove.bgposition', this._onDragBackgroundMove.bind(this));\n        $document.one('mouseup', () => {\n            this.$bgDragger.removeClass('o_we_grabbing');\n            $document.off('mousemove.bgposition');\n        });\n    },\n    /**\n     * Drags the overlay's background image, copied to target on \"Apply\".\n     *\n     * @private\n     */\n    _onDragBackgroundMove: function (ev) {\n        ev.preventDefault();\n\n        const delta = this._getBackgroundDelta();\n        this.currentPosition.left = clamp(this.currentPosition.left + ev.originalEvent.movementX, [0, delta.x]);\n        this.currentPosition.top = clamp(this.currentPosition.top + ev.originalEvent.movementY, [0, delta.y]);\n\n        const percentPosition = {\n            left: this.currentPosition.left / delta.x * 100,\n            top: this.currentPosition.top / delta.y * 100,\n        };\n        // In cover mode, one delta will be 0 and dividing by it will yield Infinity.\n        // Defaulting to originalPosition in that case (can't be dragged)\n        percentPosition.left = isFinite(percentPosition.left) ? percentPosition.left : this.originalPosition.left;\n        percentPosition.top = isFinite(percentPosition.top) ? percentPosition.top : this.originalPosition.top;\n\n        this.$bgDragger.css('background-position', `${percentPosition.left}% ${percentPosition.top}%`);\n\n        function clamp(val, bounds) {\n            // We sort the bounds because when one dimension of the rendered background is\n            // larger than the container, delta is negative, and we want to use it as lower bound\n            bounds = bounds.sort();\n            return Math.max(bounds[0], Math.min(val, bounds[1]));\n        }\n    },\n    /**\n     * Deactivates the overlay if the user clicks outside of it.\n     *\n     * @private\n     */\n    _onDocumentClicked: function (ev) {\n        if (!$(ev.target).closest('.o_we_background_position_overlay').length) {\n            this._toggleBgOverlay(false);\n        }\n    },\n});\n\n/**\n * Marks color levels of any element that may get or has a color classes. This\n * is done for the specific main colorpicker option so that those are marked on\n * snippet drop (so that base snippet definition do not need to care about that)\n * and on first focus (for compatibility).\n */\nregistry.ColoredLevelBackground = registry.BackgroundToggler.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this._markColorLevel();\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onBuilt: function () {\n        this._markColorLevel();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Adds a specific class indicating the element is colored so that nested\n     * color classes work (we support one-level). Removing it is not useful,\n     * technically the class can be added on anything that *may* receive a color\n     * class: this does not come with any CSS rule.\n     *\n     * @private\n     */\n    _markColorLevel: function () {\n        this.options.wysiwyg.odooEditor.observerUnactive('_markColorLevel');\n        this.$target.addClass('o_colored_level');\n        this.options.wysiwyg.odooEditor.observerActive('_markColorLevel');\n    },\n});\n\nregistry.ContainerWidth = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    cleanForSave: function () {\n        this.$target.removeClass('o_container_preview');\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    selectClass: async function (previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (previewMode === 'reset') {\n            this.$target.removeClass('o_container_preview');\n        } else if (previewMode) {\n            this.$target.addClass('o_container_preview');\n        }\n        this.trigger_up('option_update', {\n            optionName: 'StepsConnector',\n            name: 'change_container_width',\n        });\n    },\n});\n\n/**\n * Allows to replace a text value with the name of a database record.\n * @todo replace this mechanism with real backend m2o field ?\n */\nregistry.many2one = SnippetOptionWidget.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    /**\n     * @override\n     */\n    async willStart() {\n        const {oeMany2oneModel, oeMany2oneId} = this.$target[0].dataset;\n        this.fields = ['name', 'display_name'];\n        return Promise.all([\n            this._super(...arguments),\n            this.orm.read(oeMany2oneModel, [parseInt(oeMany2oneId)], this.fields).then(([initialRecord]) => {\n                this.initialRecord = initialRecord;\n            }),\n        ]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for params\n     */\n    async changeRecord(previewMode, widgetValue, params) {\n        const target = this.$target[0];\n        if (previewMode === 'reset') {\n            // Have to set the jQ data because it's used to update the record in other\n            // parts of the page, but have to set the dataset because used for saving.\n            this.$target.data('oeMany2oneId', this.prevId);\n            target.dataset.oeMany2oneId = this.prevId;\n            this.$target.empty().append(this.$prevContents);\n            return this._rerenderContacts(this.prevId, this.prevRecordName);\n        }\n\n        const record = JSON.parse(params.recordData);\n        if (previewMode === true) {\n            this.prevId = parseInt(target.dataset.oeMany2oneId);\n            this.$prevContents = this.$target.contents();\n            this.prevRecordName = this.prevRecordName || this.initialRecord.name;\n        }\n\n        this.$target.data('oeMany2oneId', record.id);\n        target.dataset.oeMany2oneId = record.id;\n\n        if (target.dataset.oeType !== 'contact') {\n            target.textContent = record.name;\n        }\n        await this._rerenderContacts(record.id, record.name);\n\n        if (previewMode === false) {\n            this.prevId = record.id;\n            this.$prevContents = this.$target.contents();\n            this.prevRecordName = record.name;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'changeRecord') {\n            return this.$target[0].dataset.oeMany2oneId;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        const many2oneWidget = document.createElement('we-many2one');\n        many2oneWidget.dataset.changeRecord = '';\n\n        const model = this.$target[0].dataset.oeMany2oneModel;\n        const [{name: modelName}] = await this.orm.searchRead(\"ir.model\", [['model', '=', model]], ['name']);\n        many2oneWidget.setAttribute('String', modelName);\n        many2oneWidget.dataset.model = model;\n        many2oneWidget.dataset.fields = JSON.stringify(this.fields);\n        uiFragment.appendChild(many2oneWidget);\n    },\n    /**\n     * @private\n     */\n    async _rerenderContacts(contactId, defaultText) {\n        // Rerender this same field in other places in the page (with different\n        // contact-options). Many2ones with the same contact options will just\n        // copy the HTML of the current m2o on content_changed. Not sure why we\n        // only do this for contacts, or why we do this here instead of in the\n        // wysiwyg like we do for replacing text on content_changed\n        const selector = [\n            `[data-oe-model=\"${this.$target.data('oe-model')}\"]`,\n            `[data-oe-id=\"${this.$target.data('oe-id')}\"]`,\n            `[data-oe-field=\"${this.$target.data('oe-field')}\"]`,\n            `[data-oe-contact-options!='${this.$target[0].dataset.oeContactOptions}']`,\n        ].join('');\n        let $toRerender = $(selector);\n        if (this.$target[0].dataset.oeType === 'contact') {\n            $toRerender = $toRerender.add(this.$target);\n        }\n        await Promise.all($toRerender\n            .attr('data-oe-many2one-id', contactId).data('oe-many2one-id', contactId)\n            .map(async (i, node) => {\n                if (node.dataset.oeType === 'contact') {\n                    const html = await this.orm.call(\n                        \"ir.qweb.field.contact\",\n                        \"get_record_to_html\",\n                        [[contactId]],\n                        {options: JSON.parse(node.dataset.oeContactOptions)}\n                    );\n                    $(node).html(html);\n                } else {\n                    node.textContent = defaultText;\n                }\n            }));\n    },\n});\n/**\n * Allows to display a warning message on outdated snippets.\n */\nregistry.VersionControl = SnippetOptionWidget.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Replaces an outdated snippet by its new version.\n     */\n    async replaceSnippet() {\n        // Getting the new block version.\n        let newBlockEl;\n        this.trigger_up(\"find_snippet_template\", {\n            snippet: this.$target[0],\n            callback: (snippet) => {\n                newBlockEl = snippet.baseBody.cloneNode(true);\n            },\n        });\n        // Removing the eventual dialog previews.\n        newBlockEl.querySelectorAll(\".s_dialog_preview\").forEach(previewEl => previewEl.remove());\n        // Replacing the block.\n        this.options.wysiwyg.odooEditor.historyPauseSteps();\n        this.$target[0].classList.add(\"d-none\"); // Hiding the block to replace it smoothly.\n        this.$target[0].insertAdjacentElement(\"beforebegin\", newBlockEl);\n        // Initializing the new block as if it was dropped: the mutex needs to\n        // be free for that so we wait for it first.\n        this.options.wysiwyg.waitForEmptyMutexAction().then(async () => {\n            await new Promise((resolve) => {\n                this.options.wysiwyg.snippetsMenuBus.trigger(\"CALL_POST_SNIPPET_DROP\", {\n                    $snippet: $(newBlockEl),\n                    onSuccess: resolve,\n                });\n            });\n            await new Promise(resolve => {\n                this.trigger_up(\"remove_snippet\",\n                    {$snippet: this.$target, onSuccess: resolve, shouldRecordUndo: false}\n                );\n            });\n            this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n            newBlockEl.classList.remove(\"oe_snippet_body\");\n            this.options.wysiwyg.odooEditor.historyStep();\n        });\n    },\n    /**\n     * Allows to still access the options of an outdated block, despite the\n     * warning.\n     */\n    discardAlert() {\n        const alertEl = this.$el[0].querySelector(\"we-alert\");\n        const optionsSectionEl = this.$overlay.data(\"$optionsSection\")[0];\n        alertEl.remove();\n        optionsSectionEl.classList.remove(\"o_we_outdated_block_options\");\n        // Preventing the alert to reappear at each render.\n        controlledSnippets.add(this.$target[0].dataset.snippet);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        const snippetName = this.$target[0].dataset.snippet;\n        // Do not display the alert if it was previously discarded.\n        if (controlledSnippets.has(snippetName)) {\n            return;\n        }\n        this.trigger_up(\"get_snippet_versions\", {\n            snippetName: snippetName,\n            onSuccess: snippetVersions => {\n                const isUpToDate = snippetVersions && [\"vjs\", \"vcss\", \"vxml\"].every(key => this.$target[0].dataset[key] === snippetVersions[key]);\n                if (!isUpToDate) {\n                    uiFragment.prepend(renderToElement(\"web_editor.outdated_block_message\"));\n                    // Hide the other options, to only have the alert displayed.\n                    const optionsSectionEl = this.$overlay.data(\"$optionsSection\")[0];\n                    optionsSectionEl.classList.add(\"o_we_outdated_block_options\");\n                }\n            },\n        });\n    },\n});\n\n/**\n * Handle the save of a snippet as a template that can be reused later\n */\nregistry.SnippetSave = SnippetOptionWidget.extend({\n    isTopOption: true,\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    saveSnippet: function (previewMode, widgetValue, params) {\n        return new Promise(resolve => {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"To save a snippet, we need to save all your previous modifications and reload the page.\"),\n                cancel: () => resolve(false),\n                confirmLabel: _t(\"Save and Reload\"),\n                confirm: () => {\n                    let targetCopyEl = null;\n                    const isButton = this.$target[0].matches(\"a.btn\");\n                    const snippetKey = !isButton ? this.$target[0].dataset.snippet : \"s_button\";\n                    let thumbnailURL;\n                    this.trigger_up('snippet_thumbnail_url_request', {\n                        key: snippetKey,\n                        onSuccess: url => thumbnailURL = url,\n                    });\n                    let context;\n                    this.trigger_up('context_get', {\n                        callback: ctx => context = ctx,\n                    });\n                    if (this.$target[0].matches(\".s_popup\")) {\n                        // Do not \"cleanForSave\" the popup before copying the\n                        // HTML, otherwise the popup will be saved invisible and\n                        // therefore not visible in the \"add snippet\" dialog.\n                        targetCopyEl = this.$target[0].cloneNode(true);\n                    }\n                    this.trigger_up('request_save', {\n                        reloadEditor: true,\n                        invalidateSnippetCache: true,\n                        onSuccess: async () => {\n                            const defaultSnippetName = !isButton\n                                ? _t(\"Custom %s\", this.data.snippetName)\n                                : _t(\"Custom Button\");\n                            targetCopyEl = targetCopyEl || this.$target[0].cloneNode(true);\n                            targetCopyEl.classList.add('s_custom_snippet');\n                            delete targetCopyEl.dataset.name;\n                            if (isButton) {\n                                targetCopyEl.classList.remove(\"mb-2\");\n                                targetCopyEl.classList.add(\"o_snippet_drop_in_only\", \"s_custom_button\");\n                            }\n                            // By the time onSuccess is called after request_save, the\n                            // current widget has been destroyed and is orphaned, so this._rpc\n                            // will not work as it can't trigger_up. For this reason, we need\n                            // to bypass the service provider and use the global RPC directly\n\n                            // Get editable parent TODO find proper method to get it directly\n                            let editableParentEl;\n                            for (const parentEl of this.options.getContentEditableAreas()) {\n                                if (parentEl.contains(this.$target[0])) {\n                                    editableParentEl = parentEl;\n                                    break;\n                                }\n                            }\n                            context['model'] = editableParentEl.dataset.oeModel;\n                            context['field'] = editableParentEl.dataset.oeField;\n                            context['resId'] = editableParentEl.dataset.oeId;\n                            await rpc(`/web/dataset/call_kw/ir.ui.view/save_snippet`, {\n                                model: \"ir.ui.view\",\n                                method: \"save_snippet\",\n                                args: [],\n                                kwargs: {\n                                    'name': defaultSnippetName,\n                                    'arch': targetCopyEl.outerHTML,\n                                    'template_key': this.options.snippets,\n                                    'snippet_key': snippetKey,\n                                    'thumbnail_url': thumbnailURL,\n                                    'context': context,\n                                },\n                            });\n                        },\n                    });\n                    resolve(true);\n                },\n            });\n        });\n    },\n});\n\n/**\n * Handles the dynamic colors for dynamic SVGs.\n */\nregistry.DynamicSvg = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    start() {\n        this.$target.on('image_changed.DynamicSvg', this._onImageChanged.bind(this));\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this.$target.off('.DynamicSvg');\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets the dynamic SVG's dynamic color.\n     *\n     * @see this.selectClass for params\n     */\n    async color(previewMode, widgetValue, params) {\n        const target = this.$target[0];\n        switch (previewMode) {\n            case true:\n                this.previousSrc = target.getAttribute('src');\n                break;\n            case 'reset':\n                target.src = this.previousSrc;\n                return;\n        }\n        const newURL = new URL(target.src, window.location.origin);\n        newURL.searchParams.set(params.colorName, normalizeColor(widgetValue));\n        const src = newURL.pathname + newURL.search;\n        await loadImage(src);\n        target.src = src;\n        if (!previewMode) {\n            this.previousSrc = src;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'color':\n                return new URL(this.$target[0].src, window.location.origin).searchParams.get(params.colorName);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if ('colorName' in params) {\n            return new URL(this.$target[0].src, window.location.origin).searchParams.get(params.colorName);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeVisibility(methodName, params) {\n        return this.$target.is(\"img[src^='/html_editor/shape/'], img[src^='/web_editor/shape/']\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onImageChanged(methodName, params) {\n        return this.updateUI();\n    },\n});\n\n/**\n * Allows to handle snippets with a list of items.\n */\nregistry.MultipleItems = SnippetOptionWidget.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    async addItem(previewMode, widgetValue, params) {\n        const $target = this.$(params.item);\n        const addBeforeItem = params.addBefore === 'true';\n        if ($target.length) {\n            await new Promise(resolve => {\n                this.trigger_up('clone_snippet', {\n                    $snippet: $target,\n                    onSuccess: resolve,\n                });\n            });\n            if (addBeforeItem) {\n                $target.before($target.next());\n            }\n            if (params.selectItem !== 'false') {\n                this.trigger_up('activate_snippet', {\n                    $snippet: addBeforeItem ? $target.prev() : $target.next(),\n                });\n            }\n            this._addItemCallback($target);\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async removeItem(previewMode, widgetValue, params) {\n        const $target = this.$(params.item);\n        if ($target.length) {\n            await new Promise(resolve => {\n                this.trigger_up('remove_snippet', {\n                    $snippet: $target,\n                    onSuccess: resolve,\n                });\n            });\n            this._removeItemCallback($target);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Allows to add behaviour when item added.\n     *\n     * @private\n     * @abstract\n     * @param {jQueryElement} $target\n     */\n    _addItemCallback($target) {},\n    /**\n     * @private\n     * @abstract\n     * @param {jQueryElement} $target\n     */\n    _removeItemCallback($target) {},\n});\n\nregistry.SelectTemplate = SnippetOptionWidget.extend({\n    custom_events: Object.assign({}, SnippetOptionWidget.prototype.custom_events, {\n        'user_value_widget_opening': '_onWidgetOpening',\n    }),\n\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this.containerSelector = '';\n        this.selectTemplateWidgetName = '';\n        this.orm = this.bindService(\"orm\");\n    },\n    /**\n     * @constructor\n     */\n    async start() {\n        this.containerEl = this.containerSelector ? this.$target.find(this.containerSelector)[0] : this.$target[0];\n        this._templates = {};\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Changes the snippet layout.\n     *\n     * @see this.selectClass for parameters\n     */\n    async selectTemplate(previewMode, widgetValue, params) {\n        await this._templatesLoading;\n\n        if (previewMode === 'reset') {\n            if (!this.beforePreviewNodes) {\n                // FIXME should not be necessary: only needed because we have a\n                // strange 'reset' sent after a non-preview\n                return;\n            }\n\n            // Empty the container and restore the original content\n            while (this.containerEl.lastChild) {\n                this.containerEl.removeChild(this.containerEl.lastChild);\n            }\n            for (const node of this.beforePreviewNodes) {\n                this.containerEl.appendChild(node);\n            }\n            this.beforePreviewNodes = null;\n            return;\n        }\n\n        if (!this.beforePreviewNodes) {\n            // We are about the apply a template on non-previewed content,\n            // save that content's nodes.\n            this.beforePreviewNodes = [...this.containerEl.childNodes];\n        }\n        // Empty the container and add the template content\n        while (this.containerEl.lastChild) {\n            this.containerEl.removeChild(this.containerEl.lastChild);\n        }\n        this.containerEl.insertAdjacentHTML('beforeend', this._templates[widgetValue]);\n\n        if (!previewMode) {\n            // The original content to keep saved has to be retrieved just\n            // before the preview (if we save it now, we might miss other items\n            // added by other options or custo).\n            this.beforePreviewNodes = null;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Retrieves a template either from cache or through RPC.\n     *\n     * @private\n     * @param {string} xmlid\n     * @returns {string}\n     */\n    async _getTemplate(xmlid) {\n        if (!this._templates[xmlid]) {\n            this._templates[xmlid] = await this.orm.call(\n                \"ir.ui.view\",\n                \"render_public_asset\",\n                [`${xmlid}`, {}],\n                { context: this.options.context }\n            );\n        }\n        return this._templates[xmlid];\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onWidgetOpening(ev) {\n        if (this._templatesLoading || ev.target.getName() !== this.selectTemplateWidgetName) {\n            return;\n        }\n        const templateParams = ev.target.getMethodsParams('selectTemplate');\n        const proms = templateParams.possibleValues.map(async xmlid => {\n            if (!xmlid) {\n                return;\n            }\n            // TODO should be better and retrieve all rendering in one RPC (but\n            // those ~10 RPC are only done once per edit mode if the option is\n            // opened, so I guess this is acceptable).\n            await this._getTemplate(xmlid);\n        });\n        this._templatesLoading = Promise.all(proms);\n    },\n});\n\n/*\n * Abstract option to be extended by the Carousel and gallery options (through\n * the \"CarouselHandler\" option) that handles all the common parts (reordering\n * of elements).\n */\nregistry.GalleryHandler = SnippetOptionWidget.extend({\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles reordering of items.\n     *\n     * @override\n     */\n    notify(name, data) {\n        this._super(...arguments);\n        if (name === \"reorder_items\") {\n            const itemsEls = this._getItemsGallery();\n            const oldPosition = itemsEls.indexOf(data.itemEl);\n            if (oldPosition === 0 && data.position === \"prev\") {\n                data.position = \"last\";\n            } else if (oldPosition === itemsEls.length - 1 && data.position === \"next\") {\n                data.position = \"first\";\n            }\n            itemsEls.splice(oldPosition, 1);\n            switch (data.position) {\n                case \"first\":\n                    itemsEls.unshift(data.itemEl);\n                    break;\n                case \"prev\":\n                    itemsEls.splice(Math.max(oldPosition - 1, 0), 0, data.itemEl);\n                    break;\n                case \"next\":\n                    itemsEls.splice(oldPosition + 1, 0, data.itemEl);\n                    break;\n                case \"last\":\n                    itemsEls.push(data.itemEl);\n                    break;\n            }\n            this._reorderItems(itemsEls, itemsEls.indexOf(data.itemEl));\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called to get the items of the gallery sorted by index if any (see\n     * gallery option) or by the order on the DOM otherwise.\n     *\n     * @abstract\n     * @returns {HTMLElement[]}\n     */\n    _getItemsGallery() {},\n    /**\n     * Called to reorder the items of the gallery.\n     *\n     * @abstract\n     * @param {HTMLElement[]} itemsEls - the items to reorder.\n     * @param {integer} newItemPosition - the new position of the moved items.\n     */\n    _reorderItems(itemsEls, newItemPosition) {},\n});\n\n/*\n * Abstract option to be extended by the Carousel and gallery options that\n * handles the update of the carousel indicator.\n */\nregistry.CarouselHandler = registry.GalleryHandler.extend({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Update the carousel indicator.\n     *\n     * @private\n     * @param {integer} position - the position of the indicator to activate on\n     * the carousel.\n     */\n    _updateIndicatorAndActivateSnippet(position) {\n        const carouselEl = this.$target[0].classList.contains(\"carousel\") ? this.$target[0]\n            : this.$target[0].querySelector(\".carousel\");\n        carouselEl.classList.remove(\"slide\");\n        $(carouselEl).carousel(position);\n        const indicatorEls = this.$target[0].querySelectorAll(\".carousel-indicators > *\");\n        indicatorEls.forEach((indicatorEl, i) => {\n            indicatorEl.classList.toggle(\"active\", i === position);\n        });\n        this.trigger_up(\"activate_snippet\", {\n            $snippet: $(this.$target[0].querySelector(\".carousel-item.active img\")),\n            ifInactiveOptions: true,\n        });\n        carouselEl.classList.add(\"slide\");\n        // Prevent the carousel from automatically sliding afterwards.\n        $(carouselEl).carousel(\"pause\");\n    },\n});\n\n\nexport default {\n    SnippetOptionWidget: SnippetOptionWidget,\n    snippetOptionRegistry: registry,\n\n    NULL_ID: NULL_ID,\n    UserValueWidget: UserValueWidget,\n    userValueWidgetsRegistry: userValueWidgetsRegistry,\n    UnitUserValueWidget: UnitUserValueWidget,\n\n    addTitleAndAllowedAttributes: _addTitleAndAllowedAttributes,\n    buildElement: _buildElement,\n    buildTitleElement: _buildTitleElement,\n    buildRowElement: _buildRowElement,\n    buildCollapseElement: _buildCollapseElement,\n\n    addAnimatedShapeLabel: _addAnimatedShapeLabel,\n\n    // Other names for convenience\n    Class: SnippetOptionWidget,\n    registry: registry,\n    serviceCached,\n    clearServiceCache,\n    clearControlledSnippets,\n};\n", "/** @odoo-modules **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { useChildRef, useService } from \"@web/core/utils/hooks\";\nimport { user } from \"@web/core/user\";\nimport weSnippetEditor from \"@web_editor/js/editor/snippets.editor\";\nimport wSnippetOptions from \"@website/js/editor/snippets.options\";\nimport * as OdooEditorLib from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\nimport { Component, onMounted, onWillStart, useEffect, useRef, useState } from \"@odoo/owl\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\nimport { applyTextHighlight, switchTextHighlight } from \"@website/js/text_processing\";\nimport { registry } from \"@web/core/registry\";\n\nconst snippetsEditorRegistry = registry.category(\"snippets_editor\");\nsnippetsEditorRegistry.add(\"no_parent_editor_snippets\", [\"s_popup\", \"o_mega_menu\"]);\n\nconst getDeepRange = OdooEditorLib.getDeepRange;\nconst getTraversedNodes = OdooEditorLib.getTraversedNodes;\n\nconst FontFamilyPickerUserValueWidget = wSnippetOptions.FontFamilyPickerUserValueWidget;\n\nconst ANIMATED_TEXT_SELECTOR = \".o_animated_text\";\nconst HIGHLIGHTED_TEXT_SELECTOR = \".o_text_highlight\";\n\nexport class WebsiteSnippetsMenu extends weSnippetEditor.SnippetsMenu {\n\n    static custom_events = Object.assign({}, weSnippetEditor.SnippetsMenu.custom_events, {\n        'service_context_get': '_onServiceContextGet',\n        'get_switchable_related_views': '_onGetSwitchableRelatedViews',\n        'gmap_api_request': '_onGMapAPIRequest',\n        'gmap_api_key_request': '_onGMapAPIKeyRequest',\n        'reload_bundles': '_onReloadBundles',\n    });\n\n    static tabs = Object.assign({}, weSnippetEditor.SnippetsMenu.tabs, {\n        THEME: 'theme',\n    });\n    static optionsTabStructure = [\n        ['theme-colors', _t(\"Colors\")],\n        ['website-settings', _t(\"Website\")],\n        ['theme-paragraph', _t(\"Paragraph\")],\n        ['theme-headings', _t(\"Headings\")],\n        ['theme-button', _t(\"Button\")],\n        ['theme-link', _t(\"Link\")],\n        ['theme-input', _t(\"Input Fields\")],\n        ['theme-advanced', _t(\"Advanced\")],\n    ];\n\n    static props = {\n        ...weSnippetEditor.SnippetsMenu.props,\n        getSwitchableRelatedViews: { type: Function },\n    };\n\n    static template = \"website.SnippetsMenu\";\n\n    /**\n     * @override\n     */\n    setup() {\n        super.setup();\n        this.notification = useService(\"notification\");\n        this.dialog = useService(\"dialog\");\n        this.websiteService = useService(\"website\");\n        this._notActivableElementsSelector += ', .o_mega_menu_toggle';\n\n        onWillStart(async () => {\n            this.isDesigner = await user.hasGroup(\"website.group_website_designer\");\n        });\n\n        // Displays the button that allows to highlight the animated text if\n        // there is animated text in the page.\n        useEffect(\n            () => {\n                this.state.hasAnimatedText = !!this.getEditableArea().find('.o_animated_text').length;\n            },\n            () => [this.state.isTextAnimated],\n        );\n    }\n    /**\n     * @override\n     */\n    async start() {\n        if (this.$body[0].ownerDocument !== this.ownerDocument) {\n            this.$body.on('click.snippets_menu', '*', this._onClick);\n        }\n        await super.start(...arguments);\n\n        this.__onSelectionChange = ev => {\n            this.state.isTextAnimated = this._getTextOptionState(ANIMATED_TEXT_SELECTOR);\n            this.state.isTextHighlighted = this._getTextOptionState(HIGHLIGHTED_TEXT_SELECTOR);\n        };\n        this.$body[0].ownerDocument.addEventListener('selectionchange', this.__onSelectionChange);\n\n        // Even if we prevented the drag via the css, we have to override the\n        // dragstart event because if one of the image ancestor has a dragstart\n        // listener, the dragstart handler can be called with the image as\n        // target. So we didn't prevent the drag with the css but with the\n        // following handler.\n        this.__onDragStart = ev => {\n            if (ev.target.nodeName === \"IMG\") {\n                ev.preventDefault();\n                ev.stopPropagation();\n            }\n        };\n        this.$body[0].addEventListener(\"dragstart\", this.__onDragStart);\n\n        this._adaptHighlightOnEdit = throttleForAnimation(switchTextHighlight);\n\n        // Used to adjust highlight SVGs when the text is edited.\n        this.textHighlightObserver = new MutationObserver(mutations => {\n            // We only update SVGs when the mutation targets text content\n            // (including all mutations leads to infinite loop since the\n            // highlight adjustment will also trigger observed mutations).\n            let isSVGMutation = false;\n            let isNewContentMutation = false;\n            const textHighlightEls = new Set();\n            for (const mutation of mutations) {\n                for (const addedNode of mutation.addedNodes) {\n                    const addedHighlightNode = addedNode.classlist?.contains(\"o_text_highlight\")\n                        ? addedNode\n                        : addedNode.querySelector?.(\":scope .o_text_highlight\");\n                    if (addedHighlightNode) {\n                        // E.g. When applying the split on a node with text\n                        // highlights, the `oEnter` command will split the node\n                        // and its parents correctly, which leads to duplicated\n                        // highlight items that the observer should also handle.\n                        // The goal here is to adapt these elements too.\n                        textHighlightEls.add(addedHighlightNode);\n                        isNewContentMutation = true;\n                    }\n                    if (addedNode.nodeName === \"svg\") {\n                        isSVGMutation = true;\n                    }\n                }\n                // Get the \"text highlight\" top element affected by mutations.\n                const mutationTarget = mutation.target.parentElement?.closest(\".o_text_highlight\")\n                    || mutation.target.nodeType === Node.ELEMENT_NODE\n                    && mutation.target.querySelector(\":scope .o_text_highlight\");\n                if (mutationTarget) {\n                    textHighlightEls.add(mutationTarget);\n                }\n            }\n            if (!isSVGMutation || isNewContentMutation) {\n                for (const targetEl of textHighlightEls) {\n                    this._adaptHighlightOnEdit(targetEl);\n                }\n            }\n        });\n\n        this.textHighlightObserver.observe(this.options.editable[0], {\n            attributes: false,\n            childList: true,\n            characterData: true,\n            subtree: true,\n        });\n    }\n    /**\n     * @override\n     */\n    get invalidateSnippetCache() {\n        return this.websiteService.invalidateSnippetCache;\n    }\n    set invalidateSnippetCache(value) {\n        this.websiteService.invalidateSnippetCache = value;\n    }\n    /**\n     * @override\n     */\n    onWillUnmount() {\n        super.onWillUnmount(...arguments);\n        this.$body[0].ownerDocument.removeEventListener('selectionchange', this.__onSelectionChange);\n        this.$body[0].removeEventListener(\"dragstart\", this.__onDragStart);\n        this.$body[0].classList.remove('o_animated_text_highlighted');\n        clearTimeout(this._hideBackendNavbarTimeout);\n    }\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async cleanForSave() {\n        this.textHighlightObserver.disconnect();\n        const getFromEditable = selector => this.options.editable[0].querySelectorAll(selector);\n        // Clean unstyled translations\n        return super.cleanForSave(...arguments).then(() => {\n            for (const el of getFromEditable('.o_translation_without_style')) {\n                el.classList.remove('o_translation_without_style');\n                if (el.dataset.oeTranslationSaveSha) {\n                    el.dataset.oeTranslationSourceSha = el.dataset.oeTranslationSaveSha;\n                    delete el.dataset.oeTranslationSaveSha;\n                }\n            }\n            // Adapt translation values for `select` > `options`s and remove all\n            // temporary `.o_translation_select` elements.\n            for (const optionsEl of getFromEditable('.o_translation_select')) {\n                const selectEl = optionsEl.nextElementSibling;\n                const translatedOptions = optionsEl.children;\n                const selectOptions = selectEl.tagName === 'SELECT' ? [...selectEl.options] : [];\n                if (selectOptions.length === translatedOptions.length) {\n                    selectOptions.map((option, i) => {\n                        option.text = translatedOptions[i].textContent;\n                    });\n                }\n                optionsEl.remove();\n            }\n        });\n    }\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeSnippetTemplates(html) {\n        const $html = $(html);\n\n        // TODO Remove in master. This patches the snippet move selectors.\n        const oldSelector = \".s_showcase .row:not(.s_col_no_resize) > div\";\n        let optionEl = $html[0].querySelector(`[data-js=\"SnippetMove\"][data-selector*=\"${oldSelector}\"]`);\n        if (optionEl) {\n            const newSelector = oldSelector.replace(\".row\", \".row .row\");\n            optionEl.dataset.selector = optionEl.dataset.selector.replace(oldSelector, newSelector);\n        }\n        const oldExclude = \".s_showcase .row > div\";\n        optionEl = $html[0].querySelector(`[data-js=\"SnippetMove\"][data-exclude*=\"${oldExclude}\"]`);\n        if (optionEl) {\n            const newExclude = oldExclude.replace(\".row\", \".row .row\");\n            optionEl.dataset.exclude = optionEl.dataset.exclude.replace(oldExclude, newExclude);\n        }\n\n        // TODO remove in master: changing the `data-apply-to` attribute of the\n        // grid spacing option so it is not applied on inner rows.\n        const gridSpacingOptionEls = html.querySelectorAll('[data-css-property=\"row-gap\"], [data-css-property=\"column-gap\"]');\n        gridSpacingOptionEls.forEach(gridSpacingOptionEl => gridSpacingOptionEl.dataset.applyTo = \".row.o_grid_mode\");\n\n        // TODO remove in master and adapt XML.\n        const contentAdditionEl = html.querySelector(\"#so_content_addition\");\n        if (contentAdditionEl) {\n            // Allows dropping \"inner blocks\" next to an image link.\n            contentAdditionEl.dataset.dropNear += \", div:not(.o_grid_item_image) > a:has(img)\";\n        }\n\n        const toFind = $html.find(\"we-fontfamilypicker[data-variable]\").toArray();\n        const fontVariables = toFind.map((el) => el.dataset.variable);\n        FontFamilyPickerUserValueWidget.prototype.fontVariables = fontVariables;\n\n        // TODO remove in master: adds back the \"Layout\" and \"Content Width\"\n        // options on some carousels.\n        const layoutOptionEl = html.querySelector('[data-js=\"layout_column\"][data-selector=\"section\"]');\n        const containerWidthOptionEl = html.querySelector('[data-js=\"ContainerWidth\"][data-selector=\"section\"]');\n        if (layoutOptionEl) {\n            layoutOptionEl.dataset.selector += \", section.s_carousel_wrapper .carousel-item\";\n        }\n        if (containerWidthOptionEl) {\n            containerWidthOptionEl.dataset.selector += \", .s_carousel .carousel-item\";\n        }\n\n        return super._computeSnippetTemplates(html);\n    }\n    /**\n     * Depending of the demand, reconfigure they gmap key or configure it\n     * if not already defined.\n     *\n     * @private\n     * @param {boolean} [alwaysReconfigure=false]\n     * @param {boolean} [configureIfNecessary=false]\n     */\n    async _configureGMapAPI({alwaysReconfigure, configureIfNecessary}) {\n        if (!alwaysReconfigure && !configureIfNecessary) {\n            // TODO should review, parameters are weird... only one necessary?\n            return false;\n        }\n\n        const apiKey = await new Promise(resolve => {\n            this.websiteService.websiteRootInstance.trigger_up(\"gmap_api_key_request\", {\n                onSuccess: key => resolve(key),\n            });\n        });\n        const apiKeyValidation = apiKey ? await this._validateGMapAPIKey(apiKey) : {\n            isValid: false,\n            message: undefined,\n        };\n        if (!alwaysReconfigure && configureIfNecessary && apiKey && apiKeyValidation.isValid) {\n            return false;\n        }\n\n        const websiteId = this.websiteService.currentWebsite.id;\n\n        function applyError(message) {\n            const $apiKeyInput = this.find('#api_key_input');\n            const $apiKeyHelp = this.find('#api_key_help');\n            $apiKeyInput.addClass('is-invalid');\n            $apiKeyHelp.empty().text(message);\n        }\n\n        const GoogleMapAPIKeyDialog = class extends Component {\n            static template = \"website.s_google_map_modal\";\n            static components = { Dialog };\n            static props = {\n                onMounted: Function,\n                close: Function,\n                confirm: Function\n            };\n            setup() {\n                this.modalRef = useChildRef();\n                this.state = useState({ apiKey: apiKey });\n                this.apiKeyInput = useRef(\"apiKeyInput\");\n                onMounted(() => this.props.onMounted(this.modalRef));\n            }\n            onClickSave() {\n                this.props.confirm(this.modalRef, this.state.apiKey);\n                this.props.close();\n            }\n        };\n\n        return new Promise(resolve => {\n            let invalidated = false;\n            this.dialog.add(GoogleMapAPIKeyDialog, {\n                onMounted: (modalRef) => {\n                    if (!apiKeyValidation.isValid && apiKeyValidation.message) {\n                        applyError.call($(modalRef.el), apiKeyValidation.message);\n                    }\n                },\n                confirm: async (modalRef, valueAPIKey) => {\n                    if (!valueAPIKey) {\n                        applyError.call($(modalRef.el), _t(\"Enter an API Key\"));\n                        return;\n                    }\n                    const $button = $(modalRef.el).find(\"button\");\n                    $button.prop('disabled', true);\n                    const res = await this._validateGMapAPIKey(valueAPIKey);\n                    if (res.isValid) {\n                        await this.orm.write(\"website\", [websiteId], {google_maps_api_key: valueAPIKey});\n                        invalidated = true;\n                        return true;\n                    } else {\n                        applyError.call($(modalRef.el), res.message);\n                    }\n                    $button.prop(\"disabled\", false);\n                }\n            }, {\n                onClose: () => resolve(invalidated),\n            });\n        });\n    }\n    /**\n     * @private\n     */\n    async _validateGMapAPIKey(key) {\n        try {\n            const response = await fetch(`https://maps.googleapis.com/maps/api/staticmap?center=belgium&size=10x10&key=${encodeURIComponent(key)}`);\n            const isValid = (response.status === 200);\n            return {\n                isValid: isValid,\n                message: !isValid &&\n                    _t(\"Invalid API Key. The following error was returned by Google: %(error)s\", {error: await response.text()}),\n            };\n        } catch {\n            return {\n                isValid: false,\n                message: _t(\"Check your connection and try again\"),\n            };\n        }\n    }\n    /**\n     * @override\n     */\n    _getDragAndDropOptions(options = {}) {\n        // TODO: This is currently not in use by Odoo's D&D\n        // There is currently no way in Odoo D&D to offset the edge scrolling.\n        // When there is, this code should be adapted.\n        const finalOptions = super._getDragAndDropOptions(...arguments);\n        if (!options.offsetElements || !options.offsetElements.$top) {\n            const $header = $('#top');\n            if ($header.length) {\n                finalOptions.offsetElements = finalOptions.offsetElements || {};\n                finalOptions.offsetElements.$top = $header;\n            }\n        }\n        return finalOptions;\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     * @param {string} gmapRequestEventName\n     */\n    async _handleGMapRequest(ev, gmapRequestEventName) {\n        ev.stopPropagation();\n        const reconfigured = await this._configureGMapAPI({\n            alwaysReconfigure: ev.data.reconfigure,\n            configureIfNecessary: ev.data.configureIfNecessary,\n        });\n        this.websiteService.websiteRootInstance.trigger_up(gmapRequestEventName, {\n            refetch: reconfigured,\n            editableMode: true,\n            onSuccess: key => ev.data.onSuccess(key),\n        });\n    }\n    /**\n     * Returns the text option element wrapping the selection if it exists.\n     *\n     * @private\n     * @param {String} selector\n     * @return {Element|false}\n     */\n    _getSelectedTextElement(selector) {\n        const editable = this.options.wysiwyg.$editable[0];\n        const textOptionNode = getTraversedNodes(editable).find(n => n.parentElement.closest(selector));\n        return textOptionNode ? textOptionNode.parentElement.closest(selector) : false;\n    }\n    /**\n     * @private\n     * @return {Selection|null}\n     */\n    _getSelection() {\n        return this.options.wysiwyg.odooEditor.document.getSelection();\n    }\n    /**\n     * @override\n     */\n    _addToolbar() {\n        super._addToolbar(...arguments);\n        this.state.animatedTextHighlighted = this.$body[0].classList.contains(\"o_animated_text_highlighted\");\n        this.state.isTextAnimated = this._getTextOptionState(ANIMATED_TEXT_SELECTOR);\n        this.state.isTextHighlighted = this._getTextOptionState(HIGHLIGHTED_TEXT_SELECTOR);\n\n        // As the toolbar displays css variable that are customizable by users,\n        // we have the recompute the font size selector values.\n        this.options.wysiwyg.odooEditor.computeFontSizeSelectorValues();\n    }\n    /**\n    * @override\n    */\n    _checkEditorToolbarVisibility(e) {\n        super._checkEditorToolbarVisibility(...arguments);\n        // Close the option's dropdowns manually on outside click if any open.\n        this._toolbarWrapperEl.querySelectorAll(\".dropdown-toggle.show\").forEach(toggleEl => {\n            Dropdown.getOrCreateInstance(toggleEl).hide();\n        });\n    }\n    /**\n     * Returns true if the selected text matches the selector.\n     *\n     * @private\n     */\n    _getTextOptionState(textSelector) {\n        if (!this._isValidSelection(this._getSelection())) {\n            return;\n        }\n        return !!this._getSelectedTextElement(textSelector);\n    }\n    /**\n     * @private\n     * @param {Node} node\n     * @return {Boolean}\n     */\n    _isValidSelection(sel) {\n        return sel.rangeCount && [...this.getEditableArea()].some(el => el.contains(sel.anchorNode));\n    }\n    /**\n     * @override\n     */\n    _isMobile() {\n        return this.websiteService.context.isMobile;\n    }\n    /**\n     * This callback type is used to identify the function used to apply some\n     * actions on the activated text snippet.\n     *\n     * @callback TextOptionCallback\n     * @param {jQuery} $snippet The selected text element on which the option\n     * should be applied.\n     */\n    /**\n     * Used to handle \"text options\" button click according to whether the\n     * selected text has the option activated or not.\n     *\n     * @private\n     * @param {string} classSelector\n     * @param {Array<String>} optionClassList\n     * @param {TextOptionCallback} textOptionsPostActivate callback to trigger\n     * actions when the text snippet is activated.\n     * @returns {boolean} true if the option was applied, false if it was\n     * removed or could not be applied.\n     */\n    _handleTextOptions(classSelector, optionClassList, textOptionsPostActivate = () => {}) {\n        const sel = this._getSelection();\n        if (!this._isValidSelection(sel)) {\n            return false;\n        }\n        const editable = this.options.wysiwyg.$editable[0];\n        const range = getDeepRange(editable, {splitText: true, select: true, correctTripleClick: true});\n        // Check if the text has already the current option activated.\n        let selectedTextEl = this._getSelectedTextElement(classSelector);\n        if (selectedTextEl) {\n            const restoreCursor = OdooEditorLib.preserveCursor(this.$body[0].ownerDocument);\n            // Unwrap the selected text content and disable the option.\n            const selectedTextParent = selectedTextEl.parentNode;\n            while (selectedTextEl.firstChild) {\n                const child = selectedTextEl.firstChild;\n                // When the text highlight option is activated, the text wrapper\n                // may contain SVG elements. They should be removed too...\n                if (child.nodeType === Node.ELEMENT_NODE && child.className.includes(\"o_text_highlight_item\")) {\n                    child.after(...[...child.childNodes].filter((node) => node.tagName !== \"svg\"));\n                    child.remove();\n                }\n                selectedTextParent.insertBefore(selectedTextEl.firstChild, selectedTextEl);\n            }\n            selectedTextParent.removeChild(selectedTextEl);\n            // Update the option's UI.\n            this.options.wysiwyg.odooEditor.historyResetLatestComputedSelection();\n            this.options.wysiwyg.odooEditor.historyStep(true);\n            restoreCursor();\n            if (this.options.enableTranslation) {\n                $(selectedTextParent).trigger(\"content_changed\");\n            }\n            return false;\n        } else {\n            if (sel.getRangeAt(0).collapsed) {\n                return;\n            }\n            selectedTextEl = document.createElement(\"span\");\n            selectedTextEl.classList.add(...optionClassList);\n            let $snippet = null;\n            try {\n                range.surroundContents(selectedTextEl);\n                $snippet = $(selectedTextEl);\n            } catch {\n                // This try catch is needed because 'surroundContents' may\n                // fail when the range has partially selected a non-Text node.\n                if (range.commonAncestorContainer.textContent === range.toString()) {\n                    const $commonAncestor = $(range.commonAncestorContainer);\n                    $commonAncestor.wrapInner(selectedTextEl);\n                    $snippet = $commonAncestor.find(classSelector);\n                }\n            }\n            if ($snippet) {\n                $snippet[0].normalize();\n                this._activateSnippet($snippet, false).then(() => {\n                    textOptionsPostActivate($snippet);\n                });\n                this.options.wysiwyg.odooEditor.historyStep();\n                return true;\n            } else {\n                this.notification.add(\n                    _t(\"Cannot apply this option on current text selection. Try clearing the format and try again.\"),\n                    { type: 'danger', sticky: true }\n                );\n            }\n            return false;\n        }\n    }\n    /**\n     * @private\n     * @param {string} textSelector;\n     */\n    _getOptionTextClass(textSelector) {\n        return textSelector.slice(1);\n    }\n    /**\n     * The goal here is to disable parents editors for snippets that should not\n     * display their parents options.\n     *\n     * @override\n     */\n     _allowParentsEditors($snippet) {\n        return super._allowParentsEditors(...arguments) && !snippetsEditorRegistry.get(\"no_parent_editor_snippets\")\n            .some(snippetClass => $snippet[0].classList.contains(snippetClass));\n    }\n    /**\n     * @override\n     */\n    _insertDropzone($hook) {\n        var $hookParent = $hook.parent();\n        var $dropzone = super._insertDropzone(...arguments);\n        $dropzone.attr('data-editor-message-default', $hookParent.attr('data-editor-message-default'));\n        $dropzone.attr('data-editor-message', $hookParent.attr('data-editor-message'));\n        $dropzone.attr('data-editor-sub-message', $hookParent.attr('data-editor-sub-message'));\n        return $dropzone;\n    }\n    /**\n     * @override\n     */\n    _updateDroppedSnippet($target) {\n        // Build the highlighted text content for the snippets.\n        for (const textEl of $target[0]?.querySelectorAll(\".o_text_highlight\") || []) {\n            applyTextHighlight(textEl);\n        }\n        return super._updateDroppedSnippet(...arguments);\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onGMapAPIRequest(ev) {\n        this._handleGMapRequest(ev, 'gmap_api_request');\n    }\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onGMapAPIKeyRequest(ev) {\n        this._handleGMapRequest(ev, 'gmap_api_key_request');\n    }\n    /**\n     * @private\n     */\n    _onThemeTabClick(ev) {\n        this._enableFakeOptionsTab(WebsiteSnippetsMenu.tabs.THEME);\n    }\n    /**\n     * @override\n     */\n    _onOptionsTabClick(ev) {\n        if (!ev.currentTarget.classList.contains('active')) {\n            this._activateSnippet(false);\n            this._mutex.exec(async () => {\n                const switchableViews = await this.props.getSwitchableRelatedViews();\n                if (switchableViews.length) {\n                    // These do not need to be awaited as we're in teh context\n                    // of the mutex.\n                    this._activateSnippet(this.$body.find('#wrapwrap > main'));\n                    return;\n                }\n                let $pageOptionsTarget = $();\n                let i = 0;\n                const pageOptions = this.templateOptions.filter(template => template.data.pageOptions);\n                while (!$pageOptionsTarget.length && i < pageOptions.length) {\n                    $pageOptionsTarget = pageOptions[i].selector.all();\n                    i++;\n                }\n                if ($pageOptionsTarget.length) {\n                    this._activateSnippet($pageOptionsTarget);\n                } else {\n                    this._activateEmptyOptionsTab();\n                }\n            });\n        }\n    }\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onAnimateTextClick(ev) {\n        const active = this._handleTextOptions(ANIMATED_TEXT_SELECTOR, [\n            this._getOptionTextClass(ANIMATED_TEXT_SELECTOR),\n            \"o_animate\",\n            \"o_animate_preview\",\n            \"o_anim_fade_in\",\n        ]);\n        this.state.isTextAnimated = active;\n    }\n    /**\n     * @private\n     */\n    _onHighlightAnimatedTextClick(ev) {\n        const highlighted = this.$body[0].classList.toggle('o_animated_text_highlighted');\n        this.state.animatedTextHighlighted = highlighted;\n        $(ev.target).toggleClass('fa-eye fa-eye-slash').toggleClass('text-success');\n    }\n    /**\n     * @private\n     */\n    _onTextHighlightClick() {\n        // To be able to open the highlights grid immediately, we need to\n        // prevent the `_onClick()` handler from closing the widget (using\n        // the `_closeWidgets()` method) right after opening it.\n        this._closeWidgets();\n        const active = this._handleTextOptions(\n            HIGHLIGHTED_TEXT_SELECTOR,\n            [\n                this._getOptionTextClass(HIGHLIGHTED_TEXT_SELECTOR),\n                \"o_text_highlight_underline\",\n                \"o_translate_inline\",\n            ],\n            ($snippet) => {\n                // TODO should be reviewed\n                $snippet.data(\"snippet-editor\")?.trigger_up(\"option_update\", {\n                    optionName: \"TextHighlight\",\n                    name: \"new_text_highlight\",\n                });\n            }\n        );\n        this.state.isTextHighlighted = active;\n    }\n    /**\n     * On reload bundles, when it's from the theme tab, destroy any\n     * snippetEditor as they might hold outdated style values. (e.g. color palettes).\n     * We do not destroy the Theme tab editors as they should have the correct\n     * values with their compute widget states.\n     * NOTE: This is a bit janky, _computeWidgetState should modify the\n     * option's widget to reflect the style accordingly. But since\n     * color_palette widget is independent of the UserValueWidget, it's hard to\n     * modify its style using the options events.\n     *\n     * @private\n     */\n    _onReloadBundles(ev) {\n        const excludeSelector = this.constructor.optionsTabStructure.map(element => element[0]).join(', ');\n        const oldSuccess = ev.data.onSuccess;\n        ev.data.onSuccess = (...args) => {\n            // Update the panel so that color previews reflect the ones used by the\n            // edited content.\n            this.props.setCSSVariables(this.el);\n            oldSuccess(...args);\n        };\n        for (const editor of this.snippetEditors) {\n            if (!editor.$target[0].matches(excludeSelector)) {\n                if (this._currentTab === this.tabs.THEME) {\n                    this._mutex.exec(() => {\n                        editor.destroy();\n                    });\n                } else {\n                    this._mutex.exec(async () => {\n                        await editor.updateOptionsUI(true);\n                    });\n                }\n            }\n        }\n    }\n    /**\n     * Notifies the website service that mobile preview is toggled.\n     * This will toggle the iframe between mobile and desktop view.\n     *\n     * @private\n     */\n    _toggleMobilePreview() {\n        this.websiteService.context.isMobile = !this.websiteService.context.isMobile;\n    }\n    /**\n     * Used by legacy widgets to fetch the state of the mobile preview.\n     *\n     * @private\n     * @param {CustomEvent} ev\n     */\n    _onServiceContextGet(ev) {\n        ev.data.callback({\n            isMobile: this.websiteService.context.isMobile,\n        });\n    }\n    /**\n     * Returns the list of views that can be toggled on the current page.\n     *\n     * @param {CustomEvent} ev\n     */\n    _onGetSwitchableRelatedViews(ev) {\n        this.props.getSwitchableRelatedViews().then(ev.data.onSuccess);\n    }\n}\n\nweSnippetEditor.SnippetEditor.include({\n    layoutElementsSelector: [\n        weSnippetEditor.SnippetEditor.prototype.layoutElementsSelector,\n        '.s_parallax_bg',\n        '.o_bg_video_container',\n    ].join(','),\n\n    /**\n     * @override\n     */\n    getName() {\n        if (this.$target[0].closest('[data-oe-field=logo]')) {\n            return _t(\"Logo\");\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Changes some behaviors before the drag and drop.\n     *\n     * @private\n     * @override\n     * @returns {Function} a function that restores what was changed when the\n     *  drag and drop is over.\n     */\n    _prepareDrag() {\n        const restore = this._super(...arguments);\n        // Remove the footer scroll effect if it has one (because the footer\n        // dropzone flickers otherwise when it is in grid mode).\n        const wrapwrapEl = this.$body[0].ownerDocument.defaultView.document.body.querySelector('#wrapwrap');\n        const hasFooterScrollEffect = wrapwrapEl && wrapwrapEl.classList.contains('o_footer_effect_enable');\n        if (hasFooterScrollEffect) {\n            wrapwrapEl.classList.remove('o_footer_effect_enable');\n            return () => {\n                wrapwrapEl.classList.add('o_footer_effect_enable');\n                restore();\n            };\n        }\n        return restore;\n    },\n});\n\nexport default {\n    SnippetsMenu: WebsiteSnippetsMenu,\n};\n", "/** @odoo-module **/\n\nimport { loadCSS } from \"@web/core/assets\";\nimport { AutoComplete } from \"@web/core/autocomplete/autocomplete\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport { Dialog } from \"@web/core/dialog/dialog\";\nimport { rpc } from \"@web/core/network/rpc\";\nimport { user } from \"@web/core/user\";\nimport { useChildRef } from \"@web/core/utils/hooks\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport { NavbarLinkPopoverWidget } from \"@website/js/widgets/link_popover_widget\";\nimport wUtils from \"@website/js/utils\";\nimport {\n    applyModifications,\n    isImageSupportedForStyle,\n    loadImageInfo,\n} from \"@web_editor/js/editor/image_processing\";\nimport \"@website/snippets/s_popup/options\";\nimport { range } from \"@web/core/utils/numbers\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pyToJsLocale } from \"@web/core/l10n/utils\";\nimport {Domain} from \"@web/core/domain\";\nimport {\n    isCSSColor,\n    convertCSSColorToRgba,\n    convertRgbaToCSSColor,\n    convertRgbToHsl,\n    convertHslToRgb,\n } from '@web/core/utils/colors';\nimport { renderToElement, renderToFragment } from \"@web/core/utils/render\";\nimport { browser } from \"@web/core/browser/browser\";\nimport {\n    removeTextHighlight,\n    drawTextHighlightSVG,\n} from \"@website/js/text_processing\";\nimport { throttleForAnimation } from \"@web/core/utils/timing\";\n\nimport { Component, markup, useEffect, useRef, useState } from \"@odoo/owl\";\n\nconst InputUserValueWidget = options.userValueWidgetsRegistry['we-input'];\nconst SelectUserValueWidget = options.userValueWidgetsRegistry['we-select'];\nconst Many2oneUserValueWidget = options.userValueWidgetsRegistry['we-many2one'];\n\noptions.UserValueWidget.include({\n    loadMethodsData() {\n        this._super(...arguments);\n\n        // Method names are sorted alphabetically by default. Exception here:\n        // we make sure, customizeWebsiteVariable is considered after\n        // customizeWebsiteViews so that the variable is used to show to active\n        // value when both methods are used at the same time.\n        // TODO find a better way.\n        const indexVariable = this._methodsNames.indexOf('customizeWebsiteVariable');\n        if (indexVariable >= 0) {\n            const indexView = this._methodsNames.indexOf('customizeWebsiteViews');\n            if (indexView >= 0) {\n                this._methodsNames[indexVariable] = 'customizeWebsiteViews';\n                this._methodsNames[indexView] = 'customizeWebsiteVariable';\n            }\n        }\n    },\n});\n\nMany2oneUserValueWidget.include({\n    init() {\n        this._super(...arguments);\n        this.fields = this.bindService(\"field\");\n    },\n\n    /**\n     * @override\n     */\n    async _getSearchDomain() {\n        // Add the current website's domain if the model has a website_id field.\n        // Note that the `_rpc` method is cached in Many2X user value widget,\n        // see `_rpcCache`.\n        const websiteIdField = await this.fields.loadFields(this.options.model, {\n            fieldNames: [\"website_id\"],\n        });\n        const modelHasWebsiteId = !!websiteIdField[\"website_id\"];\n        if (modelHasWebsiteId && !this.options.domain.find(arr => arr[0] === \"website_id\")) {\n            this.options.domain =\n                Domain.and([this.options.domain, wUtils.websiteDomain(this)]).toList();\n        }\n        return this.options.domain;\n    },\n});\n\nconst UrlPickerUserValueWidget = InputUserValueWidget.extend({\n    events: Object.assign({}, InputUserValueWidget.prototype.events || {}, {\n        'click .o_we_redirect_to': '_onRedirectTo',\n    }),\n\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n        const linkButton = document.createElement('we-button');\n        const icon = document.createElement('i');\n        icon.classList.add('fa', 'fa-fw', 'fa-external-link');\n        linkButton.classList.add('o_we_redirect_to', 'o_we_link', 'ms-1');\n        linkButton.title = _t(\"Preview this URL in a new tab\");\n        linkButton.appendChild(icon);\n        this.containerEl.after(linkButton);\n        this.el.classList.add('o_we_large');\n        this.inputEl.classList.add('text-start');\n        const options = {\n            classes: {\n                \"ui-autocomplete\": 'o_website_ui_autocomplete'\n            },\n            body: this.getParent().$target[0].ownerDocument.body,\n            urlChosen: this._onWebsiteURLChosen.bind(this),\n        };\n        this.unmountAutocompleteWithPages = wUtils.autocompleteWithPages(this.inputEl, options);\n    },\n\n    open() {\n        this._super(...arguments);\n        document.querySelector(\".o_website_ui_autocomplete\")?.classList?.remove(\"d-none\");\n    },\n\n    close() {\n        this._super(...arguments);\n        document.querySelector(\".o_website_ui_autocomplete\")?.classList?.add(\"d-none\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when the autocomplete change the input value.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onWebsiteURLChosen: function (ev) {\n        this._value = this.inputEl.value;\n        this._onUserValueChange(ev);\n    },\n    /**\n     * Redirects to the URL the widget currently holds.\n     *\n     * @private\n     */\n    _onRedirectTo: function () {\n        if (this._value) {\n            window.open(this._value, '_blank');\n        }\n    },\n    destroy() {\n        this.unmountAutocompleteWithPages?.();\n        this.unmountAutocompleteWithPages = null;\n        this._super(...arguments);\n    }\n});\n\nclass GoogleFontAutoComplete extends AutoComplete {\n    setup() {\n        super.setup();\n        this.inputRef = useRef(\"input\");\n        this.sourcesListRef = useRef(\"sourcesList\");\n        useEffect((el) => {\n            el.setAttribute(\"id\", \"google_font\");\n        }, () => [this.inputRef.el]);\n    }\n\n    get dropdownOptions() {\n        return {\n            ...super.dropdownOptions,\n            position: \"bottom-fit\",\n        };\n    }\n\n    onInput(ev) {\n        super.onInput(ev);\n        if (this.sourcesListRef.el) {\n            this.sourcesListRef.el.scrollTop = 0;\n        }\n    }\n}\n\nconst FontFamilyPickerUserValueWidget = SelectUserValueWidget.extend({\n    events: Object.assign({}, SelectUserValueWidget.prototype.events || {}, {\n        'click .o_we_add_font_btn': '_onAddFontClick',\n        'click .o_we_delete_font_btn': '_onDeleteFontClick',\n    }),\n    fontVariables: [], // Filled by editor menu when all options are loaded\n\n    /**\n     * @override\n     */\n    init() {\n        this.dialog = this.bindService(\"dialog\");\n        this.orm = this.bindService(\"orm\");\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        const style = window.getComputedStyle(this.$target[0].ownerDocument.documentElement);\n        const nbFonts = parseInt(weUtils.getCSSVariableValue('number-of-fonts', style));\n        // User fonts served by google server.\n        const googleFontsProperty = weUtils.getCSSVariableValue('google-fonts', style);\n        this.googleFonts = googleFontsProperty ? googleFontsProperty.split(/\\s*,\\s*/g) : [];\n        this.googleFonts = this.googleFonts.map(font => font.substring(1, font.length - 1)); // Unquote\n        // Local user fonts.\n        const googleLocalFontsProperty = weUtils.getCSSVariableValue('google-local-fonts', style);\n        this.googleLocalFonts = googleLocalFontsProperty ?\n            googleLocalFontsProperty.slice(1, -1).split(/\\s*,\\s*/g) : [];\n        const uploadedLocalFontsProperty = weUtils.getCSSVariableValue('uploaded-local-fonts', style);\n        this.uploadedLocalFonts = uploadedLocalFontsProperty ?\n            uploadedLocalFontsProperty.slice(1, -1).split(/\\s*,\\s*/g) : [];\n        // If a same font exists both remotely and locally, we remove the remote\n        // font to prioritize the local font. The remote one will never be\n        // displayed or loaded as long as the local one exists.\n        this.googleFonts = this.googleFonts.filter(font => {\n            const localFonts = this.googleLocalFonts.map(localFont => localFont.split(\":\")[0]);\n            return localFonts.indexOf(`'${font}'`) === -1;\n        });\n        this.allFonts = [];\n\n        await this._super(...arguments);\n\n        const fontsToLoad = [];\n        for (const font of this.googleFonts) {\n            const fontURL = `https://fonts.googleapis.com/css?family=${encodeURIComponent(font).replace(/%20/g, '+')}`;\n            fontsToLoad.push(fontURL);\n        }\n        for (const font of this.googleLocalFonts) {\n            const attachmentId = font.split(/\\s*:\\s*/)[1];\n            const fontURL = `/web/content/${encodeURIComponent(attachmentId)}`;\n            fontsToLoad.push(fontURL);\n        }\n        // TODO ideally, remove the <link> elements created once this widget\n        // instance is destroyed (although it should not hurt to keep them for\n        // the whole backend lifecycle).\n        const proms = fontsToLoad.map(async fontURL => loadCSS(fontURL));\n        const fontsLoadingProm = Promise.all(proms);\n\n        const fontEls = [];\n        const methodName = this.el.dataset.methodName || 'customizeWebsiteVariable';\n        const variable = this.el.dataset.variable;\n        const themeFontsNb = nbFonts - (this.googleLocalFonts.length + this.googleFonts.length + this.uploadedLocalFonts.length);\n        for (let fontNb = 0; fontNb < nbFonts; fontNb++) {\n            const realFontNb = fontNb + 1;\n            const fontKey = weUtils.getCSSVariableValue(`font-number-${realFontNb}`, style);\n            this.allFonts.push(fontKey);\n            let fontName = fontKey.slice(1, -1); // Unquote\n            let fontFamily = fontName;\n            const isSystemFonts = fontName === \"SYSTEM_FONTS\";\n            if (isSystemFonts) {\n                fontName = _t(\"System Fonts\");\n                fontFamily = 'var(--o-system-fonts)';\n            }\n            const fontEl = document.createElement('we-button');\n            fontEl.setAttribute('string', fontName);\n            fontEl.dataset.variable = variable;\n            fontEl.dataset[methodName] = fontKey;\n            fontEl.dataset.fontFamily = fontFamily;\n            const iconWrapperEl = document.createElement(\"div\");\n            iconWrapperEl.classList.add(\"text-end\");\n            fontEl.appendChild(iconWrapperEl);\n            if ((realFontNb <= themeFontsNb) && !isSystemFonts) {\n                // Add the \"cloud\" icon next to the theme's default fonts\n                // because they are served by Google.\n                iconWrapperEl.appendChild(Object.assign(document.createElement('i'), {\n                    role: 'button',\n                    className: 'text-info me-2 fa fa-cloud',\n                    title: _t(\"This font is hosted and served to your visitors by Google servers\"),\n                }));\n            }\n            fontEls.push(fontEl);\n            this.menuEl.appendChild(fontEl);\n        }\n\n        if (this.uploadedLocalFonts.length) {\n            const uploadedLocalFontsEls = fontEls.splice(-this.uploadedLocalFonts.length);\n            uploadedLocalFontsEls.forEach((el, index) => {\n                $(el).find(\".text-end\").append(renderToFragment('website.delete_font_btn', {\n                    index: index,\n                    local: \"uploaded\",\n                }));\n            });\n        }\n\n        if (this.googleLocalFonts.length) {\n            const googleLocalFontsEls = fontEls.splice(-this.googleLocalFonts.length);\n            googleLocalFontsEls.forEach((el, index) => {\n                $(el).find(\".text-end\").append(renderToFragment('website.delete_font_btn', {\n                    index: index,\n                    local: \"google\",\n                }));\n            });\n        }\n\n        if (this.googleFonts.length) {\n            const googleFontsEls = fontEls.splice(-this.googleFonts.length);\n            googleFontsEls.forEach((el, index) => {\n                $(el).find(\".text-end\").append(renderToFragment('website.delete_font_btn', {\n                    index: index,\n                }));\n            });\n        }\n\n        $(this.menuEl).append($(renderToElement('website.add_font_btn', {\n            variable: variable,\n        })));\n\n        return fontsLoadingProm;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n\n        this.menuTogglerEl.style.fontFamily = '';\n        const activeWidget = this._userValueWidgets.find(widget => !widget.isPreviewed() && widget.isActive());\n        if (activeWidget) {\n            this.menuTogglerEl.style.fontFamily = activeWidget.el.dataset.fontFamily;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    async _onAddFontClick(ev) {\n        const addFontDialog = class extends Component {\n            static template = \"website.dialog.addFont\";\n            static components = { GoogleFontAutoComplete, Dialog };\n            static props = { close: Function, title: String, onClickSave: Function };\n            state = useState({\n                valid: true, loading: false,\n                googleFontFamily: undefined, googleServe: true,\n                uploadedFontName: undefined, uploadedFonts: [], uploadedFontFaces: undefined,\n                previewText: _t(\"The quick brown fox jumps over the lazy dog.\"),\n            });\n            fileInput = useRef(\"fileInput\");\n            async onClickSave() {\n                if (this.state.loading) {\n                    return;\n                }\n                this.state.loading = true;\n                const shouldClose = await this.props.onClickSave(this.state);\n                if (shouldClose) {\n                    this.props.close();\n                    return;\n                }\n                this.state.loading = false;\n            }\n            onClickCancel() {\n                this.props.close();\n            }\n            get getGoogleFontList() {\n                return [{options: async (term) => {\n                    if (!this.googleFontList) {\n                        await rpc(\"/website/google_font_metadata\").then((data) => {\n                            this.googleFontList = data.familyMetadataList.map((font) => font.family);\n                        });\n                    }\n                    const lowerCaseTerm = term.toLowerCase();\n                    const filtered = this.googleFontList.filter((value) => value.toLowerCase().includes(lowerCaseTerm));\n                    return filtered.map((fontFamilyName) => {\n                        return {\n                            label: fontFamilyName,\n                            value: fontFamilyName,\n                        };\n                    });\n                }}];\n            }\n            async onGoogleFontSelect(selected) {\n                this.fileInput.el.value = \"\";\n                this.state.uploadedFonts = [];\n                this.state.uploadedFontName = undefined;\n                this.state.uploadedFontFaces = undefined;\n                try {\n                    const fontFamily = selected.value;\n                    const result = await fetch(`https://fonts.googleapis.com/css?family=${encodeURIComponent(fontFamily)}:300,300i,400,400i,700,700i`, {method: 'HEAD'});\n                    // Google fonts server returns a 400 status code if family is not valid.\n                    if (result.ok) {\n                        const linkId = `previewFont${fontFamily}`;\n                        if (!document.querySelector(`link[id='${linkId}']`)) {\n                            const linkEl = document.createElement(\"link\");\n                            linkEl.id = linkId;\n                            linkEl.setAttribute(\"href\", result.url);\n                            linkEl.setAttribute(\"rel\", \"stylesheet\");\n                            linkEl.dataset.fontPreview = true;\n                            document.head.appendChild(linkEl);\n                        }\n                        this.state.googleFontFamily = fontFamily;\n                    } else {\n                        this.state.googleFontFamily = undefined;\n                    }\n                } catch (error) {\n                    console.error(error);\n                }\n            }\n            async onUploadChange(e) {\n                this.state.googleFontFamily = undefined;\n                const file = this.fileInput.el.files[0];\n                if (!file) {\n                    this.state.uploadedFonts = [];\n                    this.state.uploadedFontName = undefined;\n                    this.state.uploadedFontFaces = undefined;\n                    return;\n                }\n                const reader = new FileReader();\n                reader.onload = (e) => {\n                    const base64 = e.target.result.split(',')[1];\n                    rpc(\"/website/theme_upload_font\", {\n                        name: file.name,\n                        data: base64,\n                    }).then(result => {\n                        this.state.uploadedFonts = result;\n                        this.updateFontStyle(file.name.substr(0, file.name.lastIndexOf(\".\")));\n                    });\n                };\n                reader.readAsDataURL(file);\n            }\n            /**\n             * Deduces the style of uploaded fonts and creates inline style\n             * elements in the backend iframe's head to make the font-faces\n             * available for preview.\n             *\n             * @param baseFontName\n             */\n            updateFontStyle(baseFontName) {\n                const targetFonts = {};\n                // Add candidate tags to fonts.\n                let shortestNamedFont;\n                for (const font of this.state.uploadedFonts) {\n                    if (!shortestNamedFont || font.name.length < shortestNamedFont.name.length) {\n                        shortestNamedFont = font;\n                    }\n                    font.isItalic = /italic/i.test(font.name);\n                    font.isLight = /light|300/i.test(font.name);\n                    font.isBold = /bold|700/i.test(font.name);\n                    font.isRegular = /regular|400/i.test(font.name);\n                    font.weight = font.isRegular ? 400 : font.isLight ? 300 : font.isBold ? 700 : undefined;\n                    if (font.isItalic && !font.weight) {\n                        if (!/00|thin|medium|black|condense|extrude/i.test(font.name)) {\n                            font.isRegular = true;\n                            font.weight = 400;\n                        }\n                    }\n                    font.style = font.isItalic ? \"italic\" : \"normal\";\n                    if (font.weight) {\n                        targetFonts[`${font.weight}${font.style}`] = font;\n                    }\n                }\n                if (!Object.values(targetFonts).filter((font) => font.isRegular).length) {\n                    // Keep font with shortest name.\n                    shortestNamedFont.weight = 400;\n                    shortestNamedFont.style = \"normal\";\n                    targetFonts[\"400\"] = shortestNamedFont;\n                }\n                const fontFaces = [];\n                for (const font of Object.values(targetFonts)) {\n                    fontFaces.push(`@font-face{\n                        font-family: ${baseFontName};\n                        font-style: ${font.style};\n                        font-weight: ${font.weight};\n                        src:url(\"${font.url}\");\n                    }`);\n                }\n                let styleEl = document.head.querySelector(`style[id='WebsiteThemeFontPreview-${baseFontName}']`);\n                if (!styleEl) {\n                    styleEl = document.createElement(\"style\");\n                    styleEl.id = `WebsiteThemeFontPreview-${baseFontName}`;\n                    styleEl.dataset.fontPreview = true;\n                    document.head.appendChild(styleEl);\n                }\n                const previewFontFaces = fontFaces.join(\"\");\n                styleEl.textContent = previewFontFaces;\n                this.state.uploadedFontName = baseFontName;\n                this.state.uploadedFontFaces = previewFontFaces;\n            }\n        };\n        const variable = $(ev.currentTarget).data('variable');\n        this.dialog.add(addFontDialog, {\n            title: _t(\"Add a Google font or upload a custom font\"),\n            onClickSave: async (state) => {\n                const uploadedFontName = state.uploadedFontName;\n                const uploadedFontFaces = state.uploadedFontFaces;\n                let font = undefined;\n                if (uploadedFontName && uploadedFontFaces) {\n                    const fontExistsLocally = this.uploadedLocalFonts.some(localFont => localFont.split(':')[0] === `'${uploadedFontName}'`);\n                    if (fontExistsLocally) {\n                        this.dialog.add(ConfirmationDialog, {\n                            title: _t(\"Font exists\"),\n                            body: _t(\"This uploaded font already exists.\\nTo replace an existing font, remove it first.\"),\n                        });\n                        return;\n                    }\n                    const homonymGoogleFontExists =\n                        this.googleFonts.some(font => font === uploadedFontName) ||\n                        this.googleLocalFonts.some(font => font.split(':')[0] === `'${uploadedFontName}'`);\n                    if (homonymGoogleFontExists) {\n                        this.dialog.add(ConfirmationDialog, {\n                            title: _t(\"Font name already used\"),\n                            body: _t(\"A font with the same name already exists.\\nTry renaming the uploaded file.\"),\n                        });\n                        return;\n                    }\n                    // Create attachment.\n                    const [fontCssId] = await this.orm.call(\"ir.attachment\", \"create_unique\", [[{\n                        name: uploadedFontName,\n                        description: `CSS font face for ${uploadedFontName}`,\n                        datas: btoa(uploadedFontFaces),\n                        res_model: \"ir.attachment\",\n                        mimetype: \"text/css\",\n                        \"public\": true,\n                    }]]);\n                    this.uploadedLocalFonts.push(`'${uploadedFontName}': ${fontCssId}`);\n                    font = uploadedFontName;\n                } else {\n                    let isValidFamily = false;\n                    font = state.googleFontFamily;\n\n                    try {\n                        const result = await fetch(\"https://fonts.googleapis.com/css?family=\" + encodeURIComponent(font) + ':300,300i,400,400i,700,700i', {method: 'HEAD'});\n                        // Google fonts server returns a 400 status code if family is not valid.\n                        if (result.ok) {\n                            isValidFamily = true;\n                        }\n                    } catch (error) {\n                        console.error(error);\n                    }\n\n                    if (!isValidFamily) {\n                        this.dialog.add(ConfirmationDialog, {\n                            title: _t(\"Font access\"),\n                            body: _t(\"The selected font cannot be accessed.\"),\n                        });\n                        return;\n                    }\n\n                    const googleFontServe = state.googleServe;\n                    const fontName = `'${font}'`;\n                    // If the font already exists, it will only be added if\n                    // the user chooses to add it locally when it is already\n                    // imported from the Google Fonts server.\n                    const fontExistsLocally = this.googleLocalFonts.some(localFont => localFont.split(':')[0] === fontName);\n                    const fontExistsOnServer = this.allFonts.includes(fontName);\n                    const preventFontAddition = fontExistsLocally || (fontExistsOnServer && googleFontServe);\n                    if (preventFontAddition) {\n                        this.dialog.add(ConfirmationDialog, {\n                            title: _t(\"Font exists\"),\n                            body: _t(\"This font already exists, you can only add it as a local font to replace the server version.\"),\n                        });\n                        return;\n                    }\n                    if (googleFontServe) {\n                        this.googleFonts.push(font);\n                    } else {\n                        this.googleLocalFonts.push(`'${font}': ''`);\n                    }\n                }\n                this.trigger_up('fonts_custo_request', {\n                    values: {[variable]: `'${font}'`},\n                    googleFonts: this.googleFonts,\n                    googleLocalFonts: this.googleLocalFonts,\n                    uploadedLocalFonts: this.uploadedLocalFonts,\n                });\n                let styleEl = document.head.querySelector(`[id='WebsiteThemeFontPreview-${font}']`);\n                if (styleEl) {\n                    delete styleEl.dataset.fontPreview;\n                }\n                return true;\n            },\n        },\n        {\n            onClose: () => {\n                for (const el of document.head.querySelectorAll(\"[data-font-preview]\")) {\n                    el.remove();\n                }\n            },\n        });\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onDeleteFontClick: async function (ev) {\n        ev.preventDefault();\n        const values = {};\n\n        const save = await new Promise(resolve => {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"Deleting a font requires a reload of the page. This will save all your changes and reload the page, are you sure you want to proceed?\"),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n\n        // Remove Google font\n        const fontIndex = parseInt(ev.target.dataset.fontIndex);\n        const localFont = ev.target.dataset.localFont;\n        let fontName;\n        if (localFont === 'uploaded') {\n            const font = this.uploadedLocalFonts[fontIndex].split(':');\n            // Remove double quotes\n            fontName = font[0].substring(1, font[0].length - 1);\n            values['delete-font-attachment-id'] = font[1];\n            this.uploadedLocalFonts.splice(fontIndex, 1);\n        } else if (localFont === 'google') {\n            const googleFont = this.googleLocalFonts[fontIndex].split(':');\n            // Remove double quotes\n            fontName = googleFont[0].substring(1, googleFont[0].length - 1);\n            values['delete-font-attachment-id'] = googleFont[1];\n            this.googleLocalFonts.splice(fontIndex, 1);\n        } else {\n            fontName = this.googleFonts[fontIndex];\n            this.googleFonts.splice(fontIndex, 1);\n        }\n\n        // Adapt font variable indexes to the removal\n        const style = window.getComputedStyle(this.$target[0].ownerDocument.documentElement);\n        FontFamilyPickerUserValueWidget.prototype.fontVariables.forEach((variable) => {\n            const value = weUtils.getCSSVariableValue(variable, style);\n            if (value.substring(1, value.length - 1) === fontName) {\n                // If an element is using the google font being removed, reset\n                // it to the theme default.\n                values[variable] = 'null';\n            }\n        });\n\n        this.trigger_up('fonts_custo_request', {\n            values: values,\n            googleFonts: this.googleFonts,\n            googleLocalFonts: this.googleLocalFonts,\n            uploadedLocalFonts: this.uploadedLocalFonts,\n        });\n    },\n});\n\nconst GPSPicker = InputUserValueWidget.extend({\n    // Explicitly not consider all InputUserValueWidget events. E.g. we actually\n    // don't want input focusout messing with the google map API. Because of\n    // this, clicking on google map autocomplete suggestion on Firefox was not\n    // working properly.\n    events: {},\n\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this._gmapCacheGPSToPlace = {};\n\n        // The google API will be loaded inside the website iframe. Let's try\n        // not having to load it in the backend too and just using the iframe\n        // google object instead.\n        this.contentWindow = this.$target[0].ownerDocument.defaultView;\n\n        this.notification = this.bindService(\"notification\");\n    },\n    /**\n     * @override\n     */\n    async willStart() {\n        await this._super(...arguments);\n        this._gmapLoaded = await new Promise(resolve => {\n            this.trigger_up('gmap_api_request', {\n                editableMode: true,\n                configureIfNecessary: true,\n                onSuccess: key => {\n                    if (!key) {\n                        resolve(false);\n                        return;\n                    }\n\n                    // TODO see _notifyGMapError, this tries to trigger an error\n                    // early but this is not consistent with new gmap keys.\n                    this._nearbySearch('(50.854975,4.3753899)', !!key)\n                        .then(place => resolve(!!place));\n                },\n            });\n        });\n        if (!this._gmapLoaded && !this._gmapErrorNotified) {\n            this.trigger_up('user_value_widget_critical');\n            return;\n        }\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.el.classList.add('o_we_large');\n        if (!this._gmapLoaded) {\n            return;\n        }\n\n        this._gmapAutocomplete = new this.contentWindow.google.maps.places.Autocomplete(this.inputEl, {types: ['geocode']});\n        this.contentWindow.google.maps.event.addListener(this._gmapAutocomplete, 'place_changed', this._onPlaceChanged.bind(this));\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n\n        // Without this, the google library injects elements inside the backend\n        // DOM but do not remove them once the editor is left. Notice that\n        // this is also done when the widget is destroyed for another reason\n        // than leaving the editor, but if the google API needs that container\n        // again afterwards, it will simply recreate it.\n        for (const el of document.body.querySelectorAll('.pac-container')) {\n            el.remove();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams: function (methodName) {\n        return Object.assign({gmapPlace: this._gmapPlace || {}}, this._super(...arguments));\n    },\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n        if (!this._gmapLoaded) {\n            return;\n        }\n\n        this._gmapPlace = await this._nearbySearch(this._value);\n\n        if (this._gmapPlace) {\n            this.inputEl.value = this._gmapPlace.formatted_address;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {string} gps\n     * @param {boolean} [notify=true]\n     * @returns {Promise}\n     */\n    async _nearbySearch(gps, notify = true) {\n        if (this._gmapCacheGPSToPlace[gps]) {\n            return this._gmapCacheGPSToPlace[gps];\n        }\n\n        const p = gps.substring(1).slice(0, -1).split(',');\n        const location = new this.contentWindow.google.maps.LatLng(p[0] || 0, p[1] || 0);\n        return new Promise(resolve => {\n            const service = new this.contentWindow.google.maps.places.PlacesService(document.createElement('div'));\n            service.nearbySearch({\n                // Do a 'nearbySearch' followed by 'getDetails' to avoid using\n                // GMap Geocoder which the user may not have enabled... but\n                // ideally Geocoder should be used to get the exact location at\n                // those coordinates and to limit billing query count.\n                location: location,\n                radius: 1,\n            }, (results, status) => {\n                const GMAP_CRITICAL_ERRORS = [\n                    this.contentWindow.google.maps.places.PlacesServiceStatus.REQUEST_DENIED,\n                    this.contentWindow.google.maps.places.PlacesServiceStatus.UNKNOWN_ERROR\n                ];\n                if (status === this.contentWindow.google.maps.places.PlacesServiceStatus.OK) {\n                    service.getDetails({\n                        placeId: results[0].place_id,\n                        fields: ['geometry', 'formatted_address'],\n                    }, (place, status) => {\n                        if (status === this.contentWindow.google.maps.places.PlacesServiceStatus.OK) {\n                            this._gmapCacheGPSToPlace[gps] = place;\n                            resolve(place);\n                        } else if (GMAP_CRITICAL_ERRORS.includes(status)) {\n                            if (notify) {\n                                this._notifyGMapError();\n                            }\n                            resolve();\n                        }\n                    });\n                } else if (GMAP_CRITICAL_ERRORS.includes(status)) {\n                    if (notify) {\n                        this._notifyGMapError();\n                    }\n                    resolve();\n                } else {\n                    resolve();\n                }\n            });\n        });\n    },\n    /**\n     * Indicates to the user there is an error with the google map API and\n     * re-opens the configuration dialog. For good measures, this also notifies\n     * a critical error which normally removes the related snippet entirely.\n     *\n     * @private\n     */\n    _notifyGMapError() {\n        // TODO this should be better to detect all errors. This is random.\n        // When misconfigured (wrong APIs enabled), sometimes Google throw\n        // errors immediately (which then reaches this code), sometimes it\n        // throws them later (which then induces an error log in the console\n        // and random behaviors).\n        if (this._gmapErrorNotified) {\n            return;\n        }\n        this._gmapErrorNotified = true;\n\n        this.notification.add(\n            _t(\"A Google Map error occurred. Make sure to read the key configuration popup carefully.\"),\n            { type: 'danger', sticky: true }\n        );\n        this.trigger_up('gmap_api_request', {\n            editableMode: true,\n            reconfigure: true,\n            onSuccess: () => {\n                this._gmapErrorNotified = false;\n            },\n        });\n\n        setTimeout(() => this.trigger_up('user_value_widget_critical'));\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onPlaceChanged(ev) {\n        const gmapPlace = this._gmapAutocomplete.getPlace();\n        if (gmapPlace && gmapPlace.geometry) {\n            this._gmapPlace = gmapPlace;\n            const location = this._gmapPlace.geometry.location;\n            const oldValue = this._value;\n            this._value = `(${location.lat()},${location.lng()})`;\n            this._gmapCacheGPSToPlace[this._value] = gmapPlace;\n            if (oldValue !== this._value) {\n                this._onUserValueChange(ev);\n            }\n        }\n    },\n});\noptions.userValueWidgetsRegistry['we-urlpicker'] = UrlPickerUserValueWidget;\noptions.userValueWidgetsRegistry['we-fontfamilypicker'] = FontFamilyPickerUserValueWidget;\noptions.userValueWidgetsRegistry['we-gpspicker'] = GPSPicker;\n\n//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n\noptions.Class.include({\n    custom_events: Object.assign({}, options.Class.prototype.custom_events || {}, {\n        'fonts_custo_request': '_onFontsCustoRequest',\n    }),\n    specialCheckAndReloadMethodsNames: ['customizeWebsiteViews', 'customizeWebsiteVariable', 'customizeWebsiteColor'],\n\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        // Since the website is displayed in an iframe, its jQuery\n        // instance is not the same as the editor. This property allows\n        // for easy access to bootstrap plugins (Carousel, Modal, ...).\n        // This is only needed because jQuery doesn't send custom events\n        // the same way native javascript does. So if a jQuery instance\n        // triggers a custom event, only that same jQuery instance will\n        // trigger handlers set with `.on`.\n        this.$bsTarget = this.ownerDocument.defaultView.$(this.$target[0]);\n\n        this.orm = this.bindService(\"orm\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    customizeWebsiteViews: async function (previewMode, widgetValue, params) {\n        await this._customizeWebsite(previewMode, widgetValue, params, 'views');\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    customizeWebsiteVariable: async function (previewMode, widgetValue, params) {\n        await this._customizeWebsite(previewMode, widgetValue, params, 'variable');\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    customizeWebsiteVariables: async function (previewMode, widgetValue, params) {\n        await this._customizeWebsite(previewMode, widgetValue, params, 'variables');\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    customizeWebsiteColor: async function (previewMode, widgetValue, params) {\n        await this._customizeWebsite(previewMode, widgetValue, params, 'color');\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async customizeWebsiteAssets(previewMode, widgetValue, params) {\n        await this._customizeWebsite(previewMode, widgetValue, params, 'assets');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _checkIfWidgetsUpdateNeedReload(widgets) {\n        const needReload = await this._super(...arguments);\n        if (needReload) {\n            return needReload;\n        }\n        for (const widget of widgets) {\n            const methodsNames = widget.getMethodsNames();\n            const methodNamesToCheck = this.data.pageOptions\n                ? methodsNames\n                : methodsNames.filter(m => this.specialCheckAndReloadMethodsNames.includes(m));\n            if (methodNamesToCheck.some(m => widget.getMethodsParams(m).reload)) {\n                return true;\n            }\n        }\n        return false;\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState: async function (methodName, params) {\n        switch (methodName) {\n            case 'customizeWebsiteViews': {\n                return this._getEnabledCustomizeValues(params.possibleValues, true);\n            }\n            case 'customizeWebsiteVariable': {\n                const ownerDocument = this.$target[0].ownerDocument;\n                const style = ownerDocument.defaultView.getComputedStyle(ownerDocument.documentElement);\n                let finalValue = weUtils.getCSSVariableValue(params.variable, style);\n                if (!params.colorNames) {\n                    return finalValue;\n                }\n                let tempValue = finalValue;\n                while (tempValue) {\n                    finalValue = tempValue;\n                    tempValue = weUtils.getCSSVariableValue(tempValue.replaceAll(\"'\", ''), style);\n                }\n                return finalValue;\n            }\n            case 'customizeWebsiteColor': {\n                const ownerDocument = this.$target[0].ownerDocument;\n                const style = ownerDocument.defaultView.getComputedStyle(ownerDocument.documentElement);\n                return weUtils.getCSSVariableValue(params.color, style);\n            }\n            case 'customizeWebsiteAssets': {\n                return this._getEnabledCustomizeValues(params.possibleValues, false);\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _customizeWebsite: async function (previewMode, widgetValue, params, type) {\n        // Never allow previews for theme customizations\n        if (previewMode) {\n            return;\n        }\n\n        switch (type) {\n            case 'views':\n                await this._customizeWebsiteData(widgetValue, params, true);\n                break;\n            case 'variable':\n                // Color values (e.g. \"header-text-color\") must be saved as\n                // string. TODO: Color values should be added to the color map.\n                if (params.colorNames?.includes(widgetValue)) {\n                    widgetValue =`'${widgetValue}'`;\n                }\n                await this._customizeWebsiteVariable(widgetValue, params);\n                break;\n            case \"variables\":\n                const defaultVariables = params.defaultVariables ?\n                    Object.fromEntries(params.defaultVariables.split(\",\")\n                        .map((variable) => variable.split(\":\").map(v => v.trim()))) :\n                    {};\n                const overriddenVariables = Object.fromEntries(widgetValue.split(\",\")\n                    .map((variable) => variable.split(\":\").map(v => v.trim())));\n                const variables = Object.assign(defaultVariables, overriddenVariables);\n                await this._customizeWebsiteVariables(variables, params.nullValue);\n                break;\n            case 'color':\n                await this._customizeWebsiteColor(widgetValue, params);\n                break;\n            case 'assets':\n                await this._customizeWebsiteData(widgetValue, params, false);\n                break;\n            default:\n                if (params.customCustomization) {\n                    await params.customCustomization.call(this, widgetValue, params);\n                }\n        }\n\n        if (params.reload || params.noBundleReload) {\n            // Caller will reload the page, nothing needs to be done anymore.\n            return;\n        }\n        await this._refreshBundles();\n    },\n    /**\n     * @private\n     */\n    async _refreshBundles() {\n        // Finally, only update the bundles as no reload is required\n        await this._reloadBundles();\n\n        // Some public widgets may depend on the variables that were\n        // customized, so we have to restart them *all*.\n        await new Promise((resolve, reject) => {\n            this.trigger_up('widgets_start_request', {\n                editableMode: true,\n                onSuccess: () => resolve(),\n                onFailure: () => reject(),\n            });\n        });\n    },\n    /**\n     * @private\n     */\n    async _customizeWebsiteColor(color, params) {\n        await this._customizeWebsiteColors({[params.color]: color}, params);\n    },\n    /**\n     * @private\n     */\n     async _customizeWebsiteColors(colors, params) {\n        colors = colors || {};\n\n        const baseURL = '/website/static/src/scss/options/colors/';\n        const colorType = params.colorType ? (params.colorType + '_') : '';\n        const url = `${baseURL}user_${colorType}color_palette.scss`;\n\n        const finalColors = {};\n        for (const [colorName, color] of Object.entries(colors)) {\n            finalColors[colorName] = color;\n            if (color) {\n                if (weUtils.isColorCombinationName(color)) {\n                    finalColors[colorName] = parseInt(color);\n                } else if (!isCSSColor(color)) {\n                    finalColors[colorName] = `'${color}'`;\n                }\n            }\n        }\n        return this._makeSCSSCusto(url, finalColors, params.nullValue);\n    },\n    /**\n     * @private\n     */\n    _customizeWebsiteVariable: async function (value, params) {\n        return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', {\n            [params.variable]: value,\n        }, params.nullValue);\n    },\n    /**\n     * Customizes several website variables at the same time.\n     *\n     * @private\n     * @param {Object} values: value per key variable\n     * @param {string} nullValue: string that represent null\n     */\n    _customizeWebsiteVariables: async function (values, nullValue) {\n        await this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values, nullValue);\n        await this._refreshBundles();\n    },\n    /**\n     * @private\n     */\n    async _customizeWebsiteData(value, params, isViewData) {\n        const allDataKeys = this._getDataKeysFromPossibleValues(params.possibleValues);\n        const keysToEnable = value.split(/\\s*,\\s*/);\n        const enableDataKeys = allDataKeys.filter(value => keysToEnable.includes(value));\n        const disableDataKeys = allDataKeys.filter(value => !enableDataKeys.includes(value));\n        const resetViewArch = !!params.resetViewArch;\n\n        return rpc('/website/theme_customize_data', {\n            'is_view_data': isViewData,\n            'enable': enableDataKeys,\n            'disable': disableDataKeys,\n            'reset_view_arch': resetViewArch,\n        });\n    },\n    /**\n     * @private\n     */\n    _getDataKeysFromPossibleValues(possibleValues) {\n        const allDataKeys = [];\n        for (const dataKeysStr of possibleValues) {\n            allDataKeys.push(...dataKeysStr.split(/\\s*,\\s*/));\n        }\n        // return only unique non-empty strings\n        return allDataKeys.filter((v, i, arr) => v && arr.indexOf(v) === i);\n    },\n    /**\n     * @private\n     * @param {Array} possibleValues\n     * @param {Boolean} isViewData true = \"ir.ui.view\", false = \"ir.asset\"\n     * @returns {String}\n     */\n    async _getEnabledCustomizeValues(possibleValues, isViewData) {\n        const allDataKeys = this._getDataKeysFromPossibleValues(possibleValues);\n        const enabledValues = await rpc('/website/theme_customize_data_get', {\n            'keys': allDataKeys,\n            'is_view_data': isViewData,\n        });\n        let mostValuesStr = '';\n        let mostValuesNb = 0;\n        for (const valuesStr of possibleValues) {\n            const enableValues = valuesStr.split(/\\s*,\\s*/);\n            if (enableValues.length > mostValuesNb\n                    && enableValues.every(value => enabledValues.includes(value))) {\n                mostValuesStr = valuesStr;\n                mostValuesNb = enableValues.length;\n            }\n        }\n        return mostValuesStr; // Need to return the exact same string as in possibleValues\n    },\n    /**\n     * @private\n     */\n    _makeSCSSCusto: async function (url, values, defaultValue = 'null') {\n        Object.keys(values).forEach((key) => {\n            values[key] = values[key] || defaultValue;\n        });\n        return this.orm.call(\"web_editor.assets\", \"make_scss_customization\", [url, values]);\n    },\n    /**\n     * Refreshes all public widgets related to the given element.\n     *\n     * @private\n     * @param {jQuery} [$el=this.$target]\n     * @returns {Promise}\n     */\n    _refreshPublicWidgets: async function ($el) {\n        return new Promise((resolve, reject) => {\n            this.trigger_up('widgets_start_request', {\n                editableMode: true,\n                $target: $el || this.$target,\n                onSuccess: resolve,\n                onFailure: reject,\n            });\n        });\n    },\n    /**\n     * @private\n     */\n    _reloadBundles: async function() {\n        return new Promise((resolve, reject) => {\n            this.trigger_up('reload_bundles', {\n                onSuccess: () => resolve(),\n                onFailure: () => reject(),\n            });\n        });\n    },\n    /**\n     * @override\n     */\n    _select: async function (previewMode, widget) {\n        await this._super(...arguments);\n\n        // Some blocks flicker when we start their public widgets, so we skip\n        // the refresh for them to avoid the flickering.\n        const targetNoRefreshSelector = \".s_instagram_page\";\n        // TODO: we should review the way public widgets are restarted when\n        // converting to OWL and a new API.\n        if (this.options.isWebsite && !widget.$el.closest('[data-no-widget-refresh=\"true\"]').length\n            && !this.$target[0].matches(targetNoRefreshSelector)) {\n            // TODO the flag should be retrieved through widget params somehow\n            await this._refreshPublicWidgets();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onFontsCustoRequest(ev) {\n        const values = ev.data.values ? Object.assign({}, ev.data.values) : {};\n        const googleFonts = ev.data.googleFonts;\n        const googleLocalFonts = ev.data.googleLocalFonts;\n        const uploadedLocalFonts = ev.data.uploadedLocalFonts;\n        if (googleFonts.length) {\n            values['google-fonts'] = \"('\" + googleFonts.join(\"', '\") + \"')\";\n        } else {\n            values['google-fonts'] = 'null';\n        }\n        if (googleLocalFonts.length) {\n            values['google-local-fonts'] = \"(\" + googleLocalFonts.join(\", \") + \")\";\n        } else {\n            values['google-local-fonts'] = 'null';\n        }\n        if (uploadedLocalFonts.length) {\n            values['uploaded-local-fonts'] = \"(\" + uploadedLocalFonts.join(\", \") + \")\";\n        } else {\n            values['uploaded-local-fonts'] = 'null';\n        }\n        this.trigger_up('snippet_edition_request', {exec: async () => {\n            return this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values);\n        }});\n        this.trigger_up('request_save', {\n            reloadEditor: true,\n        });\n    },\n});\n\nfunction _getLastPreFilterLayerElement($el) {\n    // Make sure parallax and video element are considered to be below the\n    // color filters / shape\n    const $bgVideo = $el.find('> .o_bg_video_container');\n    if ($bgVideo.length) {\n        return $bgVideo[0];\n    }\n    const $parallaxEl = $el.find('> .s_parallax_bg');\n    if ($parallaxEl.length) {\n        return $parallaxEl[0];\n    }\n    return null;\n}\n\noptions.registry.BackgroundToggler.include({\n    /**\n     * Toggles background video on or off.\n     *\n     * @see this.selectClass for parameters\n     */\n    toggleBgVideo(previewMode, widgetValue, params) {\n        if (!widgetValue) {\n            this.$target.find('> .o_we_bg_filter').remove();\n            // TODO: use setWidgetValue instead of calling background directly when possible\n            const [bgVideoWidget] = this._requestUserValueWidgets('bg_video_opt');\n            const bgVideoOpt = bgVideoWidget.getParent();\n            return bgVideoOpt._setBgVideo(false, '');\n        } else {\n            // TODO: use trigger instead of el.click when possible\n            this._requestUserValueWidgets('bg_video_opt')[0].el.click();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'toggleBgVideo') {\n            return this.$target[0].classList.contains('o_background_video');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * TODO an overall better management of background layers is needed\n     *\n     * @override\n     */\n    _getLastPreFilterLayerElement() {\n        const el = _getLastPreFilterLayerElement(this.$target);\n        if (el) {\n            return el;\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.BackgroundShape.include({\n    /**\n     * TODO need a better management of background layers\n     *\n     * @override\n     */\n    _getLastPreShapeLayerElement() {\n        const el = this._super(...arguments);\n        if (el) {\n            return el;\n        }\n        return _getLastPreFilterLayerElement(this.$target);\n    },\n    /**\n     * @override\n     */\n    _removeShapeEl(shapeEl) {\n        this.trigger_up('widgets_stop_request', {\n            $target: $(shapeEl),\n        });\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.ReplaceMedia.include({\n    /**\n     * Adds an anchor to the url.\n     * Here \"anchor\" means a specific section of a page.\n     *\n     * @see this.selectClass for parameters\n     */\n    setAnchor(previewMode, widgetValue, params) {\n        const linkEl = this.$target[0].parentElement;\n        let url = linkEl.getAttribute('href');\n        url = url.split('#')[0];\n        linkEl.setAttribute('href', url + widgetValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'setAnchor') {\n            const parentEl = this.$target[0].parentElement;\n            if (parentEl.tagName === 'A') {\n                const href = parentEl.getAttribute('href') || '';\n                return href ? `#${href.split('#')[1]}` : '';\n            }\n            return '';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'media_link_anchor_opt') {\n            const parentEl = this.$target[0].parentElement;\n            const linkEl = parentEl.tagName === 'A' ? parentEl : null;\n            const href = linkEl ? linkEl.getAttribute('href') : false;\n            return href && href.startsWith('/');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Fills the dropdown with the available anchors for the page referenced in\n     * the href.\n     *\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        if (!this.options.isWebsite) {\n            return this._super(...arguments);\n        }\n        await this._super(...arguments);\n\n\n\n        const oldURLWidgetEl = uiFragment.querySelector('[data-name=\"media_url_opt\"]');\n\n        const URLWidgetEl = document.createElement('we-urlpicker');\n        // Copy attributes\n        for (const {name, value} of oldURLWidgetEl.attributes) {\n            URLWidgetEl.setAttribute(name, value);\n        }\n        URLWidgetEl.title = _t(\"Hint: Type '/' to search an existing page and '#' to link to an anchor.\");\n        oldURLWidgetEl.replaceWith(URLWidgetEl);\n\n        const hrefValue = this.$target[0].parentElement.getAttribute('href');\n        if (!hrefValue || !hrefValue.startsWith('/')) {\n            return;\n        }\n        const urlWithoutAnchor = hrefValue.split('#')[0];\n        const selectEl = document.createElement('we-select');\n        selectEl.dataset.name = 'media_link_anchor_opt';\n        selectEl.dataset.dependencies = 'media_url_opt';\n        selectEl.dataset.noPreview = 'true';\n        selectEl.classList.add('o_we_sublevel_1');\n        selectEl.setAttribute('string', _t(\"Page Anchor\"));\n        const anchors = await wUtils.loadAnchors(urlWithoutAnchor);\n        for (const anchor of anchors) {\n            const weButtonEl = document.createElement('we-button');\n            weButtonEl.dataset.setAnchor = anchor;\n            weButtonEl.textContent = anchor;\n            selectEl.append(weButtonEl);\n        }\n        URLWidgetEl.after(selectEl);\n    },\n});\n\noptions.registry.ImageTools.include({\n    async _computeWidgetVisibility(widgetName, params) {\n        if (params.optionsPossibleValues.selectStyle\n                && params.cssProperty === 'width'\n                && this.$target[0].classList.contains('o_card_img')) {\n            return false;\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.BackgroundVideo = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets the target's background video.\n     *\n     * @see this.selectClass for parameters\n     */\n    background: function (previewMode, widgetValue, params) {\n        if (previewMode === 'reset' && this.videoSrc) {\n            return this._setBgVideo(false, this.videoSrc);\n        }\n        return this._setBgVideo(previewMode, widgetValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'background') {\n            if (this.$target[0].classList.contains('o_background_video')) {\n                return this.$('> .o_bg_video_container iframe').attr('src');\n            }\n            return '';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Updates the background video used by the snippet.\n     *\n     * @private\n     * @see this.selectClass for parameters\n     * @returns {Promise}\n     */\n    _setBgVideo: async function (previewMode, value) {\n        this.$('> .o_bg_video_container').toggleClass('d-none', previewMode === true);\n\n        if (previewMode !== false) {\n            return;\n        }\n\n        this.videoSrc = value;\n        var target = this.$target[0];\n        target.classList.toggle('o_background_video', !!(value && value.length));\n        if (value && value.length) {\n            target.dataset.bgVideoSrc = value;\n        } else {\n            delete target.dataset.bgVideoSrc;\n        }\n        await this._refreshPublicWidgets();\n    },\n});\n\noptions.registry.WebsiteLevelColor = options.Class.extend({\n    specialCheckAndReloadMethodsNames: options.Class.prototype.specialCheckAndReloadMethodsNames\n        .concat(['customizeWebsiteLayer2Color']),\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this._rpc = options.serviceCached(rpc);\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async customizeWebsiteLayer2Color(previewMode, widgetValue, params) {\n        if (previewMode) {\n            return;\n        }\n        params.color = params.layerColor;\n        params.variable = params.layerGradient;\n        let color = undefined;\n        let gradient = undefined;\n        if (weUtils.isColorGradient(widgetValue)) {\n            color = '';\n            gradient = widgetValue;\n        } else {\n            color = widgetValue;\n            gradient = '';\n        }\n        await this.customizeWebsiteVariable(previewMode, gradient, params);\n        params.noBundleReload = false;\n        return this.customizeWebsiteColor(previewMode, color, params);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'customizeWebsiteLayer2Color') {\n            params.variable = params.layerGradient;\n            const gradient = await this._computeWidgetState('customizeWebsiteVariable', params);\n            if (gradient) {\n                return gradient.substring(1, gradient.length - 1); // Unquote\n            }\n            params.color = params.layerColor;\n            return this._computeWidgetState('customizeWebsiteColor', params);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        const _super = this._super.bind(this);\n        if (\n            [\n                \"footer_language_selector_label_opt\",\n                \"footer_language_selector_opt\",\n            ].includes(widgetName)\n        ) {\n            this._languages = await this._rpc.call(\"/website/get_languages\");\n            if (this._languages.length === 1) {\n                return false;\n            }\n        }\n        return _super(...arguments);\n    },\n});\n\noptions.registry.OptionsTab = options.registry.WebsiteLevelColor.extend({\n    GRAY_PARAMS: {EXTRA_SATURATION: \"gray-extra-saturation\", HUE: \"gray-hue\"},\n\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.grayParams = {};\n        this.grays = {};\n        this.orm = this.bindService(\"orm\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUI() {\n        // The bg-XXX classes have been updated (and could be updated by another\n        // option like changing color palette) -> update the preview element.\n        const ownerDocument = this.$target[0].ownerDocument;\n        const style = ownerDocument.defaultView.getComputedStyle(ownerDocument.documentElement);\n        const grayPreviewEls = this.$el.find(\".o_we_gray_preview span\");\n        for (const e of grayPreviewEls) {\n            const bgValue = weUtils.getCSSVariableValue(e.getAttribute('variable'), style);\n            e.style.setProperty(\"background-color\", bgValue, \"important\");\n        }\n\n        // If the gray palette has been generated by Odoo standard option,\n        // the hue of all gray is the same and the saturation has been\n        // increased/decreased by the same amount for all grays in\n        // comparaison with BS grays. However the system supports any\n        // gray palette.\n\n        const hues = [];\n        const saturationDiffs = [];\n        let oneHasNoSaturation = false;\n        const baseStyle = getComputedStyle(document.documentElement);\n        for (let id = 100; id <= 900; id += 100) {\n            const gray = weUtils.getCSSVariableValue(`${id}`, style);\n            const grayRGB = convertCSSColorToRgba(gray);\n            const grayHSL = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);\n\n            const baseGray = weUtils.getCSSVariableValue(`base-${id}`, baseStyle);\n            const baseGrayRGB = convertCSSColorToRgba(baseGray);\n            const baseGrayHSL = convertRgbToHsl(baseGrayRGB.red, baseGrayRGB.green, baseGrayRGB.blue);\n\n            if (grayHSL.saturation > 0.01) {\n                if (grayHSL.lightness > 0.01 && grayHSL.lightness < 99.99) {\n                    hues.push(grayHSL.hue);\n                }\n                if (grayHSL.saturation < 99.99) {\n                    saturationDiffs.push(grayHSL.saturation - baseGrayHSL.saturation);\n                }\n            } else {\n                oneHasNoSaturation = true;\n            }\n        }\n        this.grayHueIsDefined = !!hues.length;\n\n        // Average of angles: we need to take the average of found hues\n        // because even if grays are supposed to be set to the exact\n        // same hue by the Odoo editor, there might be rounding errors\n        // during the conversion from RGB to HSL as the HSL system\n        // allows to represent more colors that the RGB hexadecimal\n        // notation (also: hue 360 = hue 0 and should not be averaged to 180).\n        // This also better support random gray palettes.\n        this.grayParams[this.GRAY_PARAMS.HUE] = (!hues.length) ? 0 : Math.round((Math.atan2(\n            hues.map(hue => Math.sin(hue * Math.PI / 180)).reduce((memo, value) => memo + value, 0) / hues.length,\n            hues.map(hue => Math.cos(hue * Math.PI / 180)).reduce((memo, value) => memo + value, 0) / hues.length\n        ) * 180 / Math.PI) + 360) % 360;\n\n        // Average of found saturation diffs, or all grays have no\n        // saturation, or all grays are fully saturated.\n        this.grayParams[this.GRAY_PARAMS.EXTRA_SATURATION] = saturationDiffs.length\n            ? saturationDiffs.reduce((memo, value) => memo + value, 0) / saturationDiffs.length\n            : (oneHasNoSaturation ? -100 : 100);\n\n        await this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async customizeGray(previewMode, widgetValue, params) {\n        // Gray parameters are used *on the JS side* to compute the grays that\n        // will be saved in the database. We indeed need those grays to be\n        // computed here for faster previews so this allows to not duplicate\n        // most of the logic. Also, this gives flexibility to maybe allow full\n        // customization of grays in custo and themes. Also, this allows to ease\n        // migration if the computation here was to change: the user grays would\n        // still be unchanged as saved in the database.\n\n        this.grayParams[params.param] = parseInt(widgetValue);\n        for (let i = 1; i < 10; i++) {\n            const key = (100 * i).toString();\n            this.grays[key] = this._buildGray(key);\n        }\n\n        // Preview UI update\n        this.$el.find(\".o_we_gray_preview\").each((_, e) => {\n            e.style.setProperty(\"background-color\", this.grays[e.getAttribute('variable')], \"important\");\n        });\n\n        // Save all computed (JS side) grays in database\n        await this._customizeWebsite(previewMode, undefined, Object.assign({}, params, {\n            customCustomization: () => { // TODO this could be prettier\n                return this._customizeWebsiteColors(this.grays, Object.assign({}, params, {\n                    colorType: 'gray',\n                }));\n            },\n        }));\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async configureApiKey(previewMode, widgetValue, params) {\n        return new Promise(resolve => {\n            this.trigger_up('gmap_api_key_request', {\n                editableMode: true,\n                reconfigure: true,\n                onSuccess: () => resolve(),\n            });\n        });\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async customizeBodyBgType(previewMode, widgetValue, params) {\n        if (widgetValue === 'NONE') {\n            this.bodyImageType = 'image';\n            return this.customizeBodyBg(previewMode, '', params);\n        }\n        // TODO improve: hack to click on external image picker\n        this.bodyImageType = widgetValue;\n        const widget = this._requestUserValueWidgets(params.imagepicker)[0];\n        widget.enable();\n    },\n    /**\n     * @override\n     */\n    async customizeBodyBg(previewMode, widgetValue, params) {\n        await this._customizeWebsiteVariables({\n            'body-image-type': this.bodyImageType,\n            'body-image': widgetValue ? `'${widgetValue}'` : '',\n        }, params.nullValue);\n    },\n    async openCustomCodeDialog(previewMode, widgetValue, params) {\n        return new Promise(resolve => {\n            this.trigger_up('open_edit_head_body_dialog', {\n                onSuccess: resolve,\n            });\n        });\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async switchTheme(previewMode, widgetValue, params) {\n        const save = await new Promise(resolve => {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"Changing theme requires to leave the editor. This will save all your changes, are you sure you want to proceed? Be careful that changing the theme will reset all your color customizations.\"),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        this.trigger_up('request_save', {\n            reload: false,\n            action: 'website.theme_install_kanban_action',\n        });\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async addLanguage(previewMode, widgetValue, params) {\n        // Retrieve the website id to check by default the website checkbox in\n        // the dialog box 'action_view_base_language_install'\n        const websiteId = this.options.context.website_id;\n        const save = await new Promise((resolve) => {\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"Adding a language requires to leave the editor. This will save all your changes, are you sure you want to proceed?\"),\n                confirm: () => resolve(true),\n                cancel: () => resolve(false),\n            });\n        });\n        if (!save) {\n            return;\n        }\n        this.trigger_up(\"request_save\", {\n            reload: false,\n            action: \"base.action_view_base_language_install\",\n            options: {\n                additionalContext: {\n                    params: {\n                        website_id: websiteId,\n                        url_return: \"[lang]\",\n                    }\n                },\n            }\n        });\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async customizeButtonStyle(previewMode, widgetValue, params) {\n        await this._customizeWebsiteVariables({\n            [`btn-${params.button}-outline`]: widgetValue === \"outline\" ? \"true\" : \"false\",\n            [`btn-${params.button}-flat`]: widgetValue === \"flat\" ? \"true\" : \"false\",\n        }, params.nullValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {String} id\n     * @returns {String} the adjusted color of gray\n     */\n    _buildGray(id) {\n        // Getting base grays defined in color_palette.scss\n        const gray = weUtils.getCSSVariableValue(`base-${id}`, getComputedStyle(document.documentElement));\n        const grayRGB = convertCSSColorToRgba(gray);\n        const hsl = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);\n        const adjustedGrayRGB = convertHslToRgb(this.grayParams[this.GRAY_PARAMS.HUE],\n            Math.min(Math.max(hsl.saturation + this.grayParams[this.GRAY_PARAMS.EXTRA_SATURATION], 0), 100),\n            hsl.lightness);\n        return convertRgbaToCSSColor(adjustedGrayRGB.red, adjustedGrayRGB.green, adjustedGrayRGB.blue);\n    },\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        await this._super(...arguments);\n        const extraSaturationRangeEl = uiFragment.querySelector(`we-range[data-param=${this.GRAY_PARAMS.EXTRA_SATURATION}]`);\n        if (extraSaturationRangeEl) {\n            const baseGrays = range(100, 1000, 100).map(id => {\n                const gray = weUtils.getCSSVariableValue(`base-${id}`);\n                const grayRGB = convertCSSColorToRgba(gray);\n                const hsl = convertRgbToHsl(grayRGB.red, grayRGB.green, grayRGB.blue);\n                return {id: id, hsl: hsl};\n            });\n            const first = baseGrays[0];\n            const maxValue = baseGrays.reduce((gray, value) => {\n                return gray.hsl.saturation > value.hsl.saturation ? gray : value;\n            }, first);\n            const minValue = baseGrays.reduce((gray, value) => {\n                return gray.hsl.saturation < value.hsl.saturation ? gray : value;\n            }, first);\n            extraSaturationRangeEl.dataset.max = 100 - minValue.hsl.saturation;\n            extraSaturationRangeEl.dataset.min = -maxValue.hsl.saturation;\n        }\n    },\n    /**\n     * @override\n     */\n    async _checkIfWidgetsUpdateNeedWarning(widgets) {\n        const warningMessage = await this._super(...arguments);\n        if (warningMessage) {\n            return warningMessage;\n        }\n        for (const widget of widgets) {\n            if (widget.getMethodsNames().includes('customizeWebsiteVariable')\n                    && widget.getMethodsParams('customizeWebsiteVariable').variable === 'color-palettes-name') {\n                const hasCustomizedColors = weUtils.getCSSVariableValue('has-customized-colors');\n                if (hasCustomizedColors && hasCustomizedColors !== 'false') {\n                    return _t(\"Changing the color palette will reset all your color customizations, are you sure you want to proceed?\");\n                }\n            }\n        }\n        return '';\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'customizeBodyBgType') {\n            const bgImage = getComputedStyle(this.ownerDocument.querySelector('#wrapwrap'))['background-image'];\n            if (bgImage === 'none') {\n                return \"NONE\";\n            }\n            return weUtils.getCSSVariableValue('body-image-type');\n        }\n        if (methodName === 'customizeGray') {\n            // See updateUI override\n            return this.grayParams[params.param];\n        }\n        if (methodName === 'customizeButtonStyle') {\n            const isOutline = weUtils.getCSSVariableValue(`btn-${params.button}-outline`);\n            const isFlat = weUtils.getCSSVariableValue(`btn-${params.button}-flat`);\n            return isFlat === \"true\" ? \"flat\" : isOutline === \"true\" ? \"outline\" : \"fill\";\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'body_bg_image_opt') {\n            return false;\n        }\n        if (params.param === this.GRAY_PARAMS.HUE) {\n            return this.grayHueIsDefined;\n        }\n        if (params.removeFont) {\n            const font = await this._computeWidgetState('customizeWebsiteVariable', {\n                variable: params.removeFont,\n            });\n            return !!font;\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.ThemeColors = options.registry.OptionsTab.extend({\n    /**\n     * @override\n     */\n    async start() {\n        // Checks for support of the old color system\n        const style = window.getComputedStyle(this.$target[0].ownerDocument.documentElement);\n        const supportOldColorSystem = weUtils.getCSSVariableValue('support-13-0-color-system', style) === 'true';\n        const hasCustomizedOldColorSystem = weUtils.getCSSVariableValue('has-customized-13-0-color-system', style) === 'true';\n        this._showOldColorSystemWarning = supportOldColorSystem && hasCustomizedOldColorSystem;\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUIVisibility() {\n        await this._super(...arguments);\n        const oldColorSystemEl = this.el.querySelector('.o_old_color_system_warning');\n        oldColorSystemEl.classList.toggle('d-none', !this._showOldColorSystemWarning);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        const paletteSelectorEl = uiFragment.querySelector('[data-variable=\"color-palettes-name\"]');\n        const style = window.getComputedStyle(document.documentElement);\n        const allPaletteNames = weUtils.getCSSVariableValue('palette-names', style).split(', ').map((name) => {\n            return name.replace(/'/g, \"\");\n        });\n        for (const paletteName of allPaletteNames) {\n            const btnEl = document.createElement('we-button');\n            btnEl.classList.add('o_palette_color_preview_button');\n            btnEl.dataset.customizeWebsiteVariable = `'${paletteName}'`;\n            [1, 3, 2].forEach(c => {\n                const colorPreviewEl = document.createElement('span');\n                colorPreviewEl.classList.add('o_palette_color_preview');\n                const color = weUtils.getCSSVariableValue(`o-palette-${paletteName}-o-color-${c}`, style);\n                colorPreviewEl.style.backgroundColor = color;\n                btnEl.appendChild(colorPreviewEl);\n            });\n            paletteSelectorEl.appendChild(btnEl);\n        }\n\n        const presetCollapseEl = uiFragment.querySelector('we-collapse.o_we_theme_presets_collapse');\n        let ccPreviewEls = [];\n        for (let i = 1; i <= 5; i++) {\n            const collapseEl = document.createElement('we-collapse');\n            const ccPreviewEl = $(renderToElement('web_editor.color.combination.preview.legacy'))[0];\n            ccPreviewEl.classList.add('text-center', `o_cc${i}`, 'o_colored_level', 'o_we_collapse_toggler');\n            collapseEl.appendChild(ccPreviewEl);\n            collapseEl.appendChild(renderToFragment('website.color_combination_edition', {number: i}));\n            ccPreviewEls.push(ccPreviewEl);\n            presetCollapseEl.appendChild(collapseEl);\n        }\n        await this._super(...arguments);\n    },\n});\n\noptions.registry.menu_data = options.Class.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.notification = this.bindService(\"notification\");\n    },\n\n    /**\n     * When the users selects a menu, a popover is shown with 4 possible\n     * actions: follow the link in a new tab, copy the menu link, edit the menu,\n     * or edit the menu tree.\n     * The popover shows a preview of the menu link. Remote URL only show the\n     * favicon.\n     *\n     * @override\n     */\n    start: function () {\n        const wysiwyg = $(this.ownerDocument.getElementById('wrapwrap')).data('wysiwyg');\n        const popoverContainer = this.ownerDocument.getElementById('oe_manipulators');\n        NavbarLinkPopoverWidget.createFor({\n            target: this.$target[0],\n            wysiwyg,\n            container: popoverContainer,\n            notify: this.notification.add,\n            checkIsWebsiteDesigner: () => user.hasGroup(\"website.group_website_designer\"),\n            onEditLinkClick: (widget) => {\n                var $menu = widget.$target.find('[data-oe-id]');\n                this.trigger_up('menu_dialog', {\n                    name: $menu.text(),\n                    url: $menu.parent().attr('href'),\n                    save: (name, url) => {\n                        let websiteId;\n                        this.trigger_up('context_get', {\n                            callback: ctx => websiteId = ctx['website_id'],\n                        });\n                        const data = {\n                            id: $menu.data('oe-id'),\n                            name,\n                            url,\n                        };\n                        return this.orm.call(\n                            \"website.menu\",\n                            \"save\",\n                            [websiteId, {'data': [data]}]\n                        ).then(function () {\n                            widget.wysiwyg.odooEditor.observerUnactive();\n                            widget.$target.attr('href', url);\n                            $menu.text(name);\n                            widget.wysiwyg.odooEditor.observerActive();\n                        });\n                    },\n                });\n                widget.popover.hide();\n            },\n            onEditMenuClick: (widget) => {\n                const contentMenu = widget.target.closest('[data-content_menu_id]');\n                const rootID = contentMenu ? parseInt(contentMenu.dataset.content_menu_id, 10) : undefined;\n                this.trigger_up('action_demand', {\n                    actionName: 'edit_menu',\n                    params: [rootID],\n                });\n            },\n        });\n        return this._super(...arguments);\n    },\n    /**\n      * When the users selects another element on the page, makes sure the\n      * popover is closed.\n      *\n      * @override\n      */\n    onBlur: function () {\n        this.$target.popover('hide');\n    },\n});\n\noptions.registry.Carousel = options.registry.CarouselHandler.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this.$indicators = this.$target.find('.carousel-indicators');\n        this.$controls = this.$target.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators');\n\n        // Prevent enabling the carousel overlay when clicking on the carousel\n        // controls (indeed we want it to change the carousel slide then enable\n        // the slide overlay) + See \"CarouselItem\" option.\n        this.$controls.addClass('o_we_no_overlay');\n\n        // Handle the sliding manually.\n        this.__onControlClick = throttleForAnimation(this._onControlClick.bind(this));\n        this.$controls.on(\"click.carousel_option\", this.__onControlClick);\n\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\n        this.$bsTarget.off('.carousel_option');\n        this.$controls.off(\".carousel_option\");\n    },\n    /**\n     * @override\n     */\n    onBuilt: function () {\n        this._assignUniqueID();\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this._assignUniqueID();\n    },\n    /**\n     * @override\n     */\n    notify(name, data) {\n        this._super(...arguments);\n        if (name === 'add_slide') {\n            this._addSlide().then(data.onSuccess);\n        } else if (name === \"slide\") {\n            this._slide(data.direction).then(data.onSuccess);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    addSlide(previewMode, widgetValue, params) {\n        return this._addSlide();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Creates a unique ID for the carousel and reassign data-attributes that\n     * depend on it.\n     *\n     * @private\n     */\n    _assignUniqueID: function () {\n        const id = 'myCarousel' + Date.now();\n        this.$target.attr('id', id);\n        this.$target.find('[data-bs-target]').attr('data-bs-target', '#' + id);\n        this.$target.find('[data-bs-slide], [data-bs-slide-to]').toArray().forEach((el) => {\n            var $el = $(el);\n            if ($el.attr('data-bs-target')) {\n                $el.attr('data-bs-target', '#' + id);\n            } else if ($el.attr('href')) {\n                $el.attr('href', '#' + id);\n            }\n        });\n    },\n    /**\n     * Adds a slide.\n     *\n     * @private\n     */\n    async _addSlide() {\n        this.options.wysiwyg.odooEditor.historyPauseSteps();\n        const $items = this.$target.find('.carousel-item');\n        this.$controls.removeClass('d-none');\n        const $active = $items.filter('.active');\n        this.$indicators.append($('<button>', {\n            'data-bs-target': '#' + this.$target.attr('id'),\n            'aria-label': _t('Carousel indicator'),\n        }));\n        this.$indicators.append(' ');\n        // Need to remove editor data from the clone so it gets its own.\n        $active.clone(false)\n            .removeClass('active')\n            .insertAfter($active);\n        await this._slide(\"next\");\n        this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n    },\n    /**\n     * Slides the carousel in the given direction.\n     *\n     * @private\n     * @param {String|Number} direction the direction in which to slide:\n     *     - \"prev\": the previous slide;\n     *     - \"next\": the next slide;\n     *     - number: a slide number.\n     * @returns {Promise}\n     */\n    _slide(direction) {\n        this.trigger_up(\"disable_loading_effect\");\n        let _slideTimestamp;\n        this.$bsTarget.one(\"slide.bs.carousel\", () => {\n            _slideTimestamp = window.performance.now();\n            setTimeout(() => this.trigger_up('hide_overlay'));\n        });\n\n        return new Promise(resolve => {\n            this.$bsTarget.one(\"slid.bs.carousel\", () => {\n                // slid.bs.carousel is most of the time fired too soon by bootstrap\n                // since it emulates the transitionEnd with a setTimeout. We wait\n                // here an extra 20% of the time before retargeting edition, which\n                // should be enough...\n                const _slideDuration = (window.performance.now() - _slideTimestamp);\n                setTimeout(() => {\n                    // Setting the active indicator manually, as Bootstrap could\n                    // not do it because the `data-bs-slide-to` attribute is not\n                    // here in edit mode anymore.\n                    const $activeSlide = this.$target.find(\".carousel-item.active\");\n                    const activeIndex = [...$activeSlide[0].parentElement.children].indexOf($activeSlide[0]);\n                    const activeIndicatorEl = [...this.$indicators[0].children][activeIndex];\n                    activeIndicatorEl.classList.add(\"active\");\n                    activeIndicatorEl.setAttribute(\"aria-current\", \"true\");\n\n                    this.trigger_up(\"activate_snippet\", {\n                        $snippet: $activeSlide,\n                        ifInactiveOptions: true,\n                    });\n                    this.$bsTarget.trigger(\"active_slide_targeted\"); // TODO remove in master: kept for compatibility.\n                    this.trigger_up(\"enable_loading_effect\");\n                    resolve();\n                }, 0.2 * _slideDuration);\n            });\n\n            this.$bsTarget.carousel(direction);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Slides the carousel when clicking on the carousel controls. This handler\n     * allows to put the sliding in the mutex, to avoid race conditions.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onControlClick(ev) {\n        // Compute to which slide the carousel will slide.\n        const controlEl = ev.currentTarget;\n        let direction;\n        if (controlEl.classList.contains(\"carousel-control-prev\")) {\n            direction = \"prev\";\n        } else if (controlEl.classList.contains(\"carousel-control-next\")) {\n            direction = \"next\";\n        } else {\n            const indicatorEl = ev.target;\n            if (!indicatorEl.matches(\".carousel-indicators > *\") || indicatorEl.classList.contains(\"active\")) {\n                return;\n            }\n            direction = [...controlEl.children].indexOf(indicatorEl);\n        }\n\n        // Slide the carousel.\n        this.trigger_up(\"snippet_edition_request\", {exec: async () => {\n            this.options.wysiwyg.odooEditor.historyPauseSteps();\n            await this._slide(direction);\n            this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n            this.options.wysiwyg.odooEditor.historyStep();\n        }});\n    },\n    /**\n     * @override\n     */\n    _getItemsGallery() {\n        return Array.from(this.$target[0].querySelectorAll(\".carousel-item\"));\n    },\n    /**\n     * @override\n     */\n    _reorderItems(itemsEls, newItemPosition) {\n        const carouselInnerEl = this.$target[0].querySelector(\".carousel-inner\");\n        // First, empty the content of the carousel.\n        carouselInnerEl.replaceChildren();\n        // Then fill it with the new slides.\n        for (const itemsEl of itemsEls) {\n            carouselInnerEl.append(itemsEl);\n        }\n        this._updateIndicatorAndActivateSnippet(newItemPosition);\n    },\n\n});\n\noptions.registry.CarouselItem = options.Class.extend({\n    isTopOption: true,\n    forceNoDeleteButton: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.$carousel = this.$bsTarget.closest('.carousel');\n        this.$targetCarousel = this.$target.closest(\".carousel\");\n        this.$indicators = this.$carousel.find('.carousel-indicators');\n        this.$controls = this.$carousel.find('.carousel-control-prev, .carousel-control-next, .carousel-indicators');\n        this.carouselOptionName = this.$carousel[0].classList.contains(\"s_carousel_intro\") ? \"CarouselIntro\" : \"Carousel\";\n\n        var leftPanelEl = this.$overlay.data('$optionsSection')[0];\n        var titleTextEl = leftPanelEl.querySelector('we-title > span');\n        this.counterEl = document.createElement('span');\n        titleTextEl.appendChild(this.counterEl);\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        // Activate the active slide after removing a slide.\n        if (this.hasRemovedSlide) {\n            this.trigger_up(\"activate_snippet\", {\n                $snippet: this.$targetCarousel.find(\".carousel-item.active\"),\n                ifInactiveOptions: true,\n            });\n            this.hasRemovedSlide = false;\n        }\n        this._super(...arguments);\n        this.$carousel.off('.carousel_item_option');\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Updates the slide counter.\n     *\n     * @override\n     */\n    updateUI: async function () {\n        await this._super(...arguments);\n        const $items = this.$carousel.find('.carousel-item');\n        const $activeSlide = $items.filter('.active');\n        const updatedText = ` (${$activeSlide.index() + 1}/${$items.length})`;\n        this.counterEl.textContent = updatedText;\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    addSlideItem(previewMode, widgetValue, params) {\n        return new Promise(resolve => {\n            this.trigger_up(\"option_update\", {\n                optionName: this.carouselOptionName,\n                name: \"add_slide\",\n                data: {\n                    onSuccess: () => resolve(),\n                },\n            });\n        });\n    },\n    /**\n     * Removes the current slide.\n     *\n     * @see this.selectClass for parameters.\n     */\n    async removeSlide(previewMode) {\n        this.options.wysiwyg.odooEditor.historyPauseSteps();\n        const $items = this.$carousel.find('.carousel-item');\n        const newLength = $items.length - 1;\n        if (!this.removing && newLength > 0) {\n            // The active indicator is deleted to ensure that the other\n            // indicators will still work after the deletion.\n            const $toDelete = $items.filter('.active').add(this.$indicators.find('.active'));\n            this.removing = true; // TODO remove in master: kept for stable.\n            // Go to the previous slide.\n            await new Promise(resolve => {\n                this.trigger_up(\"option_update\", {\n                    optionName: this.carouselOptionName,\n                    name: \"slide\",\n                    data: {\n                        direction: \"prev\",\n                        onSuccess: () => resolve(),\n                    },\n                });\n            });\n            // Remove the slide.\n            $toDelete.remove();\n            this.$controls.toggleClass(\"d-none\", newLength === 1);\n            this.$carousel.trigger(\"content_changed\");\n            this.removing = false;\n        }\n        this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n        this.hasRemovedSlide = true;\n    },\n    /**\n     * Goes to next slide or previous slide.\n     *\n     * @see this.selectClass for parameters\n     */\n    switchToSlide(previewMode, widgetValue, params) {\n        this.options.wysiwyg.odooEditor.historyPauseSteps();\n        const direction = widgetValue === \"left\" ? \"prev\" : \"next\";\n        return new Promise(resolve => {\n            this.trigger_up(\"option_update\", {\n                optionName: this.carouselOptionName,\n                name: \"slide\",\n                data: {\n                    direction: direction,\n                    onSuccess: () => {\n                        this.options.wysiwyg.odooEditor.historyUnpauseSteps();\n                        resolve();\n                    },\n                },\n            });\n        });\n    },\n});\n\noptions.registry.Parallax = options.Class.extend({\n    /**\n     * @override\n     */\n    async start() {\n        this.parallaxEl = this.$target.find('> .s_parallax_bg')[0] || null;\n        this._updateBackgroundOptions();\n\n        this.$target.on('content_changed.ParallaxOption', this._onExternalUpdate.bind(this));\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onFocus() {\n        // Refresh the parallax animation on focus; at least useful because\n        // there may have been changes in the page that influenced the parallax\n        // rendering (new snippets, ...).\n        // TODO make this automatic.\n        if (this.parallaxEl) {\n            this._refreshPublicWidgets();\n        }\n    },\n    /**\n     * @override\n     */\n    onMove() {\n        this._refreshPublicWidgets();\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$target.off('.ParallaxOption');\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Build/remove parallax.\n     *\n     * @see this.selectClass for parameters\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (params.attributeName !== 'scrollBackgroundRatio') {\n            return;\n        }\n\n        const isParallax = (widgetValue !== '0');\n        this.$target.toggleClass('parallax', isParallax);\n        this.$target.toggleClass('s_parallax_is_fixed', widgetValue === '1');\n        this.$target.toggleClass('s_parallax_no_overflow_hidden', (widgetValue === '0' || widgetValue === '1'));\n        if (isParallax) {\n            if (!this.parallaxEl) {\n                this.parallaxEl = document.createElement('span');\n                this.parallaxEl.classList.add('s_parallax_bg');\n                this.$target.prepend(this.parallaxEl);\n            }\n        } else {\n            if (this.parallaxEl) {\n                this.parallaxEl.remove();\n                this.parallaxEl = null;\n            }\n        }\n\n        this._updateBackgroundOptions();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeVisibility(widgetName) {\n        return !this.$target.hasClass('o_background_video');\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'selectDataAttribute' && params.parallaxTypeOpt) {\n            const attrName = params.attributeName;\n            const attrValue = (this.$target[0].dataset[attrName] || params.attributeDefaultValue).trim();\n            switch (attrValue) {\n                case '0':\n                case '1': {\n                    return attrValue;\n                }\n                default: {\n                    return (attrValue.startsWith('-') ? '-1.5' : '1.5');\n                }\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Updates external background-related option to work with the parallax\n     * element instead of the original target when necessary.\n     *\n     * @private\n     */\n    _updateBackgroundOptions() {\n        this.trigger_up('option_update', {\n            optionNames: ['BackgroundImage', 'BackgroundPosition', 'BackgroundOptimize'],\n            name: 'target',\n            data: this.parallaxEl ? $(this.parallaxEl) : this.$target,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called on any snippet update to check if the parallax should still be\n     * enabled or not.\n     *\n     * TODO there is probably a better system to implement to solve this issue.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onExternalUpdate(ev) {\n        if (!this.parallaxEl) {\n            return;\n        }\n        const bgImage = this.parallaxEl.style.backgroundImage;\n        if (!bgImage || bgImage === 'none' || this.$target.hasClass('o_background_video')) {\n            // The parallax option was enabled but the background image was\n            // removed: disable the parallax option.\n            const widget = this._requestUserValueWidgets('parallax_none_opt')[0];\n            widget.enable();\n            widget.getParent().close(); // FIXME remove this ugly hack asap\n        }\n    },\n});\n\noptions.registry.collapse = options.Class.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        var self = this;\n        this.$bsTarget.on('shown.bs.collapse hidden.bs.collapse', '[role=\"region\"]', function () {\n            self.trigger_up('cover_update');\n            self.$target.trigger('content_changed');\n        });\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    onBuilt: function () {\n        this._createIDs();\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this._createIDs();\n    },\n    /**\n     * @override\n     */\n    onMove: function () {\n        this._createIDs();\n        var $panel = this.$bsTarget.find('.collapse').removeData('bs.collapse');\n        if ($panel.attr('aria-expanded') === 'true') {\n            $panel.closest('.accordion').find('.collapse[aria-expanded=\"true\"]')\n                .filter((i, el) => (el !== $panel[0]))\n                .collapse('hide')\n                .one('hidden.bs.collapse', function () {\n                    $panel.trigger('shown.bs.collapse');\n                });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Associates unique ids on collapse elements.\n     *\n     * @private\n     */\n    _createIDs: function () {\n        let time = new Date().getTime();\n        const accordionEl = this.$target[0].closest(\".accordion\");\n        const accordionBtnEl = this.$target[0].querySelector(\".accordion-button\");\n        const accordionContentEl = this.$target[0].querySelector('[role=\"region\"]');\n        const $body = this.$target.closest('body');\n\n        const setUniqueId = (el, label) => {\n            let elemId = el.id;\n            if (!elemId || $body.find('[id=\"' + elemId + '\"]').length > 1) {\n                do {\n                    time++;\n                    elemId = label + time;\n                } while ($body.find('#' + elemId).length);\n                el.id = elemId;\n            }\n            return elemId;\n        };\n\n        const accordionId = setUniqueId(accordionEl, \"myCollapse\");\n        accordionContentEl.dataset.bsParent = \"#\" + accordionId;\n\n        const contentId = setUniqueId(accordionContentEl, \"myCollapseTab\");\n        accordionBtnEl.dataset.bsTarget = \"#\" + contentId;\n        accordionBtnEl.setAttribute(\"aria-controls\", contentId);\n\n        const buttonId = setUniqueId(accordionBtnEl, \"myCollapseBtn\");\n        accordionContentEl.setAttribute(\"aria-labelledby\", buttonId);\n    },\n});\n\noptions.registry.HeaderElements = options.Class.extend({\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this._rpc = options.serviceCached(rpc);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        const _super = this._super.bind(this);\n        switch (widgetName) {\n            case \"header_language_selector_opt\":\n                this._languages = await this._rpc.call(\"/website/get_languages\");\n                if (this._languages.length === 1) {\n                    return false;\n                }\n                break;\n        }\n        return _super(...arguments);\n    },\n});\n\noptions.registry.HeaderNavbar = options.Class.extend({\n    /**\n     * Particular case: we want the option to be associated on the header navbar\n     * in XML so that the related options only appear on navbar click (not\n     * header), in a different section, etc... but we still want the target to\n     * be the header itself.\n     *\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this.setTarget(this.$target.closest('#wrapwrap > header'));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Needs to be done manually for now because data-dependencies\n     * doesn't work with \"AND\" conditions.\n     * TODO: improve this.\n     *\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        switch (widgetName) {\n            case 'option_logo_height_scrolled': {\n                return !!this.$('.navbar-brand').length;\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n\nconst VisibilityPageOptionUpdate = options.Class.extend({\n    pageOptionName: undefined,\n    showOptionWidgetName: undefined,\n    shownValue: '',\n\n    /**\n     * @override\n     */\n    async onTargetShow() {\n        if (await this._isShown()) {\n            // onTargetShow may be called even if the element is already shown.\n            // In most cases, this is not a problem but here it is as the code\n            // that follows clicks on the visibility checkbox regardless of its\n            // status. This avoids searching for that checkbox entirely.\n            return;\n        }\n        // TODO improve: here we make a hack so that if we make the invisible\n        // header appear for edition, its actual visibility for the page is\n        // toggled (otherwise it would be about editing an element which\n        // is actually never displayed on the page).\n        const widget = this._requestUserValueWidgets(this.showOptionWidgetName)[0];\n        widget.enable();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for params\n     */\n    async visibility(previewMode, widgetValue, params) {\n        const show = (widgetValue !== 'hidden');\n        await new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'toggle_page_option',\n                params: [{name: this.pageOptionName, value: show}],\n                onSuccess: () => resolve(),\n                onFailure: reject,\n            });\n        });\n        this.trigger_up('snippet_option_visibility_update', {show: show});\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'visibility') {\n            const shown = await this._isShown();\n            return shown ? this.shownValue : 'hidden';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     * @returns {boolean}\n     */\n    async _isShown() {\n        return new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'get_page_option',\n                params: [this.pageOptionName],\n                onSuccess: v => resolve(!!v),\n                onFailure: reject,\n            });\n        });\n    },\n});\n\noptions.registry.TopMenuVisibility = VisibilityPageOptionUpdate.extend({\n    pageOptionName: 'header_visible',\n    showOptionWidgetName: 'regular_header_visibility_opt',\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles the switching between 3 differents visibilities of the header.\n     *\n     * @see this.selectClass for params\n     */\n    async visibility(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        await this._changeVisibility(widgetValue);\n        // TODO this is hacky but changing the header visibility may have an\n        // effect on features like FullScreenHeight which depend on viewport\n        // size so we simulate a resize.\n        const targetWindow = this.$target[0].ownerDocument.defaultView;\n        targetWindow.dispatchEvent(new targetWindow.Event('resize'));\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _changeVisibility(widgetValue) {\n        const show = (widgetValue !== 'hidden');\n        if (!show) {\n            return;\n        }\n        const transparent = (widgetValue === 'transparent');\n        await new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'toggle_page_option',\n                params: [{name: 'header_overlay', value: transparent}],\n                onSuccess: () => resolve(),\n                onFailure: reject,\n            });\n        });\n        if (!transparent) {\n            return;\n        }\n        // TODO should be able to change both options at the same time, as the\n        // `params` list suggests.\n        await new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'toggle_page_option',\n                params: [{name: 'header_color', value: ''}],\n                onSuccess: () => resolve(),\n                onFailure: reject,\n            });\n        });\n        await new Promise(resolve => {\n            this.trigger_up('action_demand', {\n                actionName: 'toggle_page_option',\n                params: [{name: 'header_text_color', value: ''}],\n                onSuccess: () => resolve(),\n            });\n        });\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        const _super = this._super.bind(this);\n        if (methodName === 'visibility') {\n            this.shownValue = await new Promise((resolve, reject) => {\n                this.trigger_up('action_demand', {\n                    actionName: 'get_page_option',\n                    params: ['header_overlay'],\n                    onSuccess: v => resolve(v ? 'transparent' : 'regular'),\n                    onFailure: reject,\n                });\n            });\n        }\n        return _super(...arguments);\n    },\n});\n\noptions.registry.topMenuColor = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async selectStyle(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (widgetValue && !isCSSColor(widgetValue)) {\n            widgetValue = params.colorPrefix + widgetValue;\n        }\n        await new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'toggle_page_option',\n                params: [{name: params.pageOptionName, value: widgetValue}],\n                onSuccess: resolve,\n                onFailure: reject,\n            });\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeVisibility: async function () {\n        const show = await this._super(...arguments);\n        if (!show) {\n            return false;\n        }\n        return new Promise((resolve, reject) => {\n            this.trigger_up('action_demand', {\n                actionName: 'get_page_option',\n                params: ['header_overlay'],\n                onSuccess: value => resolve(!!value),\n                onFailure: reject,\n            });\n        });\n    },\n});\n\n/**\n * Manage the visibility of snippets on mobile/desktop.\n */\noptions.registry.DeviceVisibility = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Toggles the device visibility.\n     *\n     * @see this.selectClass for parameters\n     */\n    async toggleDeviceVisibility(previewMode, widgetValue, params) {\n        this.$target[0].classList.remove('d-none', 'd-md-none', 'd-lg-none',\n            'o_snippet_mobile_invisible', 'o_snippet_desktop_invisible',\n            'o_snippet_override_invisible',\n        );\n        const style = getComputedStyle(this.$target[0]);\n        this.$target[0].classList.remove(`d-md-${style['display']}`, `d-lg-${style['display']}`);\n        if (widgetValue === 'no_desktop') {\n            this.$target[0].classList.add('d-lg-none', 'o_snippet_desktop_invisible');\n        } else if (widgetValue === 'no_mobile') {\n            this.$target[0].classList.add(`d-lg-${style['display']}`, 'd-none', 'o_snippet_mobile_invisible');\n        }\n\n        // Update invisible elements.\n        const isMobile = wUtils.isMobile(this);\n        this.trigger_up('snippet_option_visibility_update', {show: widgetValue !== (isMobile ? 'no_mobile' : 'no_desktop')});\n    },\n    /**\n     * @override\n     */\n    async onTargetHide() {\n        this.$target[0].classList.remove('o_snippet_override_invisible');\n    },\n    /**\n     * @override\n     */\n    async onTargetShow() {\n        const isMobilePreview = weUtils.isMobileView(this.$target[0]);\n        const isMobileHidden = this.$target[0].classList.contains(\"o_snippet_mobile_invisible\");\n        if ((this.$target[0].classList.contains('o_snippet_mobile_invisible')\n                || this.$target[0].classList.contains('o_snippet_desktop_invisible')\n            ) && isMobilePreview === isMobileHidden) {\n            this.$target[0].classList.add('o_snippet_override_invisible');\n        }\n    },\n    /**\n     * @override\n     */\n    cleanForSave() {\n        this.$target[0].classList.remove('o_snippet_override_invisible');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'toggleDeviceVisibility') {\n            const classList = [...this.$target[0].classList];\n            if (classList.includes('d-none') &&\n                    classList.some(className => className.match(/^d-(md|lg)-/))) {\n                return 'no_mobile';\n            }\n            if (classList.some(className => className.match(/d-(md|lg)-none/))) {\n                return 'no_desktop';\n            }\n            return '';\n        }\n        return await this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (this.$target[0].classList.contains('s_table_of_content_main')) {\n            return false;\n        }\n        return this._super(...arguments);\n    }\n});\n\n/**\n * Hide/show footer in the current page.\n */\noptions.registry.HideFooter = VisibilityPageOptionUpdate.extend({\n    pageOptionName: 'footer_visible',\n    showOptionWidgetName: 'hide_footer_page_opt',\n    shownValue: 'shown',\n});\n\n/**\n * Handles the edition of snippet's anchor name.\n */\noptions.registry.anchor = options.Class.extend({\n    isTopOption: true,\n\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.notification = this.bindService(\"notification\");\n    },\n    /**\n     * @override\n     */\n    start() {\n        // Generate anchor and copy it to clipboard on click, show the tooltip on success\n        const buttonEl = this.el.querySelector(\"we-button\");\n        this.isModal = this.$target[0].classList.contains(\"modal\");\n        if (buttonEl && !this.isModal) {\n            this._buildClipboard(buttonEl);\n        }\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this.$target.removeAttr('data-anchor');\n        this.$target.filter(':not(.carousel)').removeAttr('id');\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    notify(name, data) {\n        this._super(...arguments);\n        if (name === \"modalAnchor\") {\n            this._buildClipboard(data.buttonEl);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Element} buttonEl\n     */\n    _buildClipboard(buttonEl) {\n        buttonEl.addEventListener(\"click\", async (ev) => {\n            const anchorLink = this._getAnchorLink();\n            await browser.navigator.clipboard.writeText(anchorLink);\n            const message = markup(_t(\"Anchor copied to clipboard<br>Link: %s\", anchorLink));\n            this.notification.add(message, {\n                type: \"success\",\n                buttons: [{name: _t(\"Edit\"), onClick: () => this._openAnchorDialog(buttonEl), primary: true}],\n            });\n        });\n    },\n\n    /**\n     * @private\n     * @param {Element} buttonEl\n     */\n    _openAnchorDialog(buttonEl) {\n        const anchorDialog = class extends Component {\n            static template = \"website.dialog.anchorName\";\n            static props = { close: Function, confirm: Function, delete: Function, currentAnchor: String };\n            static components = { Dialog };\n            title = _t(\"Link Anchor\");\n            modalRef = useChildRef();\n            onClickConfirm() {\n                const shouldClose = this.props.confirm(this.modalRef);\n                if (shouldClose) {\n                    this.props.close();\n                }\n            }\n            onClickDelete() {\n                this.props.delete();\n                this.props.close();\n            }\n            onClickDiscard() {\n                this.props.close();\n            }\n        };\n        const props = {\n            confirm: (modalRef) => {\n                const inputEl = modalRef.el.querySelector(\".o_input_anchor_name\");\n                const anchorName = this._text2Anchor(inputEl.value);\n                if (this.$target[0].id === anchorName) {\n                    // If the chosen anchor name is already the one used by the\n                    // element, close the dialog and do nothing else\n                    return true;\n                }\n\n                const alreadyExists = !!this.ownerDocument.getElementById(anchorName);\n                modalRef.el.querySelector('.o_anchor_already_exists').classList.toggle('d-none', !alreadyExists);\n                inputEl.classList.toggle('is-invalid', alreadyExists);\n                if (!alreadyExists) {\n                    this._setAnchorName(anchorName);\n                    buttonEl.click();\n                    return true;\n                }\n            },\n            currentAnchor: decodeURIComponent(this.$target.attr('id')),\n        };\n        if (this.$target.attr('id')) {\n            props[\"delete\"] = () => {\n                this._setAnchorName();\n            };\n        }\n        this.dialog.add(anchorDialog, props);\n    },\n    /**\n     * @private\n     * @param {String} value\n     */\n    _setAnchorName: function (value) {\n        if (value) {\n            this.$target[0].id = value;\n            if (!this.isModal) {\n                this.$target[0].dataset.anchor = true;\n            }\n        } else {\n            this.$target.removeAttr('id data-anchor');\n        }\n        this.$target.trigger('content_changed');\n    },\n    /**\n     * Returns anchor text.\n     *\n     * @private\n     * @returns {string}\n     */\n    _getAnchorLink: function () {\n        if (!this.$target[0].id) {\n            const $titles = this.$target.find('h1, h2, h3, h4, h5, h6');\n            const title = $titles.length > 0 ? $titles[0].innerText : this.data.snippetName;\n            const anchorName = this._text2Anchor(title);\n            let n = '';\n            while (this.ownerDocument.getElementById(anchorName + n)) {\n                n = (n || 1) + 1;\n            }\n            this._setAnchorName(anchorName + n);\n        }\n        const pathName = this.isModal ? \"\" : this.ownerDocument.location.pathname;\n        return `${pathName}#${this.$target[0].id}`;\n    },\n    /**\n     * Creates a safe id/anchor from text.\n     *\n     * @private\n     * @param {string} text\n     * @returns {string}\n     */\n    _text2Anchor: function (text) {\n        return encodeURIComponent(text.trim().replace(/\\s+/g, '-'));\n    },\n});\n\noptions.registry.HeaderBox = options.registry.Box.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async selectStyle(previewMode, widgetValue, params) {\n        if ((params.variable || params.color)\n                && ['border-width', 'border-style', 'border-color', 'border-radius', 'box-shadow'].includes(params.cssProperty)) {\n            if (previewMode) {\n                return;\n            }\n            if (params.cssProperty === 'border-color') {\n                return this.customizeWebsiteColor(previewMode, widgetValue, params);\n            }\n            return this.customizeWebsiteVariable(previewMode, widgetValue, params);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async setShadow(previewMode, widgetValue, params) {\n        if (params.variable) {\n            if (previewMode) {\n                return;\n            }\n            const defaultShadow = this._getDefaultShadow(widgetValue, params.shadowClass);\n            return this.customizeWebsiteVariable(previewMode, defaultShadow, params);\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        const value = await this._super(...arguments);\n        if (methodName === \"selectStyle\" && params.cssProperty === \"border-width\") {\n            // One-sided borders return \"0px 0px 3px 0px\", which prevents the\n            // option from being displayed properly. We only keep the affected\n            // border.\n            return value.replace(/(^|\\s)0px/gi, \"\").trim() || value;\n        }\n        return value;\n    },\n});\n\noptions.registry.CookiesBar = options.registry.SnippetPopup.extend({\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Change the cookies bar layout.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectLayout: function (previewMode, widgetValue, params) {\n        let websiteId;\n        this.trigger_up('context_get', {\n            callback: function (ctx) {\n                websiteId = ctx['website_id'];\n            },\n        });\n\n        const $template = $(renderToElement(`website.cookies_bar.${widgetValue}`, {\n            websiteId: websiteId,\n        }));\n\n        const $content = this.$target.find('.modal-content');\n\n        // The order of selectors is significant since certain selectors may be\n        // nested within others, and we want to preserve the nested ones.\n        // For instance, in the case of '.o_cookies_bar_text_policy' nested\n        // inside '.o_cookies_bar_text_secondary', the parent selector should be\n        // copied first, followed by the child selector to ensure that the\n        // content of the nested selector is not overwritten.\n        const selectorsToKeep = [\n            '.o_cookies_bar_text_button',\n            '.o_cookies_bar_text_button_essential',\n            '.o_cookies_bar_text_title',\n            '.o_cookies_bar_text_primary',\n            '.o_cookies_bar_text_secondary',\n            '.o_cookies_bar_text_policy'\n        ];\n\n        if (this.$savedSelectors === undefined) {\n            this.$savedSelectors = [];\n        }\n\n        for (const selector of selectorsToKeep) {\n            const $currentLayoutEls = $content.find(selector).contents();\n            const $newLayoutEl = $template.find(selector);\n            if ($currentLayoutEls.length) {\n                // save value before change, eg 'title' is not inside 'discrete' template\n                // but we want to preserve it in case of select another layout later\n                this.$savedSelectors[selector] = $currentLayoutEls;\n            }\n            const $savedSelector = this.$savedSelectors[selector];\n            if ($newLayoutEl.length && $savedSelector && $savedSelector.length) {\n                $newLayoutEl.empty().append($savedSelector);\n            }\n        }\n\n        $content.empty().append($template);\n    },\n});\n\n/**\n * Allows edition of 'cover_properties' in website models which have such\n * fields (blogs, posts, events, ...).\n */\noptions.registry.CoverProperties = options.Class.extend({\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this.$image = this.$target.find('.o_record_cover_image');\n        this.$filter = this.$target.find('.o_record_cover_filter');\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.$filterValueOpts = this.$el.find('[data-filter-value]');\n\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles a background change.\n     *\n     * @see this.selectClass for parameters\n     */\n    background: async function (previewMode, widgetValue, params) {\n        if (previewMode === false) {\n            this.$image[0].classList.remove(\"o_b64_image_to_save\");\n        }\n        if (widgetValue === '') {\n            this.$image.css('background-image', '');\n            this.$target.removeClass('o_record_has_cover');\n        } else {\n            if (previewMode === false) {\n                const imgEl = document.createElement(\"img\");\n                imgEl.src = widgetValue;\n                await loadImageInfo(imgEl);\n                if (imgEl.dataset.mimetype && ![\n                    \"image/gif\",\n                    \"image/svg+xml\",\n                    \"image/webp\",\n                ].includes(imgEl.dataset.mimetype)) {\n                    // Convert to webp but keep original width.\n                    imgEl.dataset.mimetype = \"image/webp\";\n                    const base64src = await applyModifications(imgEl, {\n                        mimetype: \"image/webp\",\n                    });\n                    widgetValue = base64src;\n                    this.$image[0].classList.add(\"o_b64_image_to_save\");\n                }\n            }\n            this.$image.css('background-image', `url('${widgetValue}')`);\n            this.$target.addClass('o_record_has_cover');\n            const $defaultSizeBtn = this.$el.find('.o_record_cover_opt_size_default');\n            $defaultSizeBtn.click();\n            $defaultSizeBtn.closest('we-select').click();\n        }\n\n        if (!previewMode) {\n            this._updateSavingDataset();\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    filterValue: function (previewMode, widgetValue, params) {\n        this.$filter.css('opacity', widgetValue || 0);\n        this.$filter.toggleClass('oe_black', parseFloat(widgetValue) !== 0);\n\n        if (!previewMode) {\n            this._updateSavingDataset();\n        }\n    },\n    /**\n     * @override\n     */\n    selectStyle: async function (previewMode, widgetValue, params) {\n        await this._super(...arguments);\n\n        if (!previewMode) {\n            this._updateSavingDataset(widgetValue);\n        }\n    },\n    /**\n     * @override\n     */\n    selectClass: async function (previewMode, widgetValue, params) {\n        await this._super(...arguments);\n\n        if (!previewMode) {\n            this._updateSavingDataset();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'filterValue': {\n                return parseFloat(this.$filter.css('opacity')).toFixed(1);\n            }\n            case 'background': {\n                const background = this.$image.css('background-image');\n                if (background && background !== 'none') {\n                    return background.match(/^url\\([\"']?(.+?)[\"']?\\)$/)[1];\n                }\n                return '';\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility: function (widgetName, params) {\n        if (params.coverOptName) {\n            return this.$target.data(`use_${params.coverOptName}`) === 'True';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _updateColorDataset(bgColorStyle = '', bgColorClass = '') {\n        this.$target[0].dataset.bgColorStyle = bgColorStyle;\n        this.$target[0].dataset.bgColorClass = bgColorClass;\n    },\n    /**\n     * Updates the cover properties dataset used for saving.\n     *\n     * @private\n     */\n    _updateSavingDataset(colorValue) {\n        const [colorPickerWidget, sizeWidget, textAlignWidget] = this._requestUserValueWidgets('bg_color_opt', 'size_opt', 'text_align_opt');\n        // TODO: `o_record_has_cover` should be handled using model field, not\n        // resize_class to avoid all of this.\n        // Get values from DOM (selected values in options are only available\n        // after updateUI)\n        const sizeOptValues = sizeWidget.getMethodsParams('selectClass').possibleValues;\n        let coverClass = [...this.$target[0].classList].filter(\n            value => sizeOptValues.includes(value)\n        ).join(' ');\n        const bg = this.$image.css('background-image');\n        if (bg && bg !== 'none') {\n            coverClass += \" o_record_has_cover\";\n        }\n        const textAlignOptValues = textAlignWidget.getMethodsParams('selectClass').possibleValues;\n        const textAlignClass = [...this.$target[0].classList].filter(\n            value => textAlignOptValues.includes(value)\n        ).join(' ');\n        const filterEl = this.$target[0].querySelector('.o_record_cover_filter');\n        const filterValue = filterEl && filterEl.style.opacity;\n        // Update saving dataset\n        this.$target[0].dataset.coverClass = coverClass;\n        this.$target[0].dataset.textAlignClass = textAlignClass;\n        this.$target[0].dataset.filterValue = filterValue || 0.0;\n        // TODO there is probably a better way and this should be refactored to\n        // use more standard colorpicker+imagepicker structure\n        const ccValue = colorPickerWidget._ccValue;\n        const colorOrGradient = colorPickerWidget._value;\n        const isGradient = weUtils.isColorGradient(colorOrGradient);\n        const valueIsCSSColor = !isGradient && isCSSColor(colorOrGradient);\n        const colorNames = [];\n        if (ccValue) {\n            colorNames.push(ccValue);\n        }\n        if (colorOrGradient && !isGradient && !valueIsCSSColor) {\n            colorNames.push(colorOrGradient);\n        }\n        const bgColorClass = weUtils.computeColorClasses(colorNames).join(' ');\n        const bgColorStyle = valueIsCSSColor ? `background-color: ${colorOrGradient};` :\n            isGradient ? `background-color: rgba(0, 0, 0, 0); background-image: ${colorOrGradient};` : '';\n        this._updateColorDataset(bgColorStyle, bgColorClass);\n    },\n});\n\noptions.registry.ScrollButton = options.Class.extend({\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n        this.$button = this.$('.o_scroll_button');\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    async showScrollButton(previewMode, widgetValue, params) {\n        if (widgetValue) {\n            this.$button.show();\n        } else {\n            if (previewMode) {\n                this.$button.hide();\n            } else {\n                this.$button.detach();\n            }\n        }\n    },\n    /**\n     * Toggles the scroll down button.\n     */\n    toggleButton: function (previewMode, widgetValue, params) {\n        if (widgetValue) {\n            if (!this.$button.length) {\n                const anchor = document.createElement('a');\n                anchor.classList.add(\n                    'o_scroll_button',\n                    'mb-3',\n                    'rounded-circle',\n                    'align-items-center',\n                    'justify-content-center',\n                    'mx-auto',\n                    'bg-primary',\n                    'o_not_editable',\n                );\n                anchor.href = '#';\n                anchor.contentEditable = \"false\";\n                anchor.title = _t(\"Scroll down to next section\");\n                const arrow = document.createElement('i');\n                arrow.classList.add('fa', 'fa-angle-down', 'fa-3x');\n                anchor.appendChild(arrow);\n                this.$button = $(anchor);\n            }\n            this.$target.append(this.$button);\n        } else {\n            this.$button.detach();\n        }\n    },\n    /**\n     * @override\n     */\n    async selectClass(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        // If a \"d-lg-block\" class exists on the section (e.g., for mobile\n        // visibility option), it should be replaced with a \"d-lg-flex\" class.\n        // This ensures that the section has the \"display: flex\" property\n        // applied, which is the default rule for both \"height\" option classes.\n        if (params.possibleValues.includes(\"o_half_screen_height\")) {\n            if (widgetValue) {\n                this.$target[0].classList.replace(\"d-lg-block\", \"d-lg-flex\");\n            } else if (this.$target[0].classList.contains(\"d-lg-flex\")) {\n                // There are no known cases, but we still make sure that the\n                // <section> element doesn't have a \"display: flex\" originally.\n                this.$target[0].classList.remove(\"d-lg-flex\");\n                const sectionStyle = window.getComputedStyle(this.$target[0]);\n                const hasDisplayFlex = sectionStyle.getPropertyValue(\"display\") === \"flex\";\n                this.$target[0].classList.add(hasDisplayFlex ? \"d-lg-flex\" : \"d-lg-block\");\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        // TODO We should have a better way to change labels depending on some\n        // condition (maybe a dedicated way in updateUI...)\n        if (this.$target[0].dataset.snippet === 's_image_gallery') {\n            const minHeightEl = uiFragment.querySelector('[data-name=\"minheight_auto_opt\"]');\n            minHeightEl.parentElement.setAttribute('string', _t(\"Min-Height\"));\n        }\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'toggleButton':\n                return !!this.$button.parent().length;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'fixed_height_opt') {\n            return (this.$target[0].dataset.snippet === 's_image_gallery');\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.ConditionalVisibility = options.registry.DeviceVisibility.extend({\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this.optionsAttributes = [];\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n\n        for (const widget of this._userValueWidgets) {\n            const params = widget.getMethodsParams();\n            if (params.saveAttribute) {\n                this.optionsAttributes.push({\n                    saveAttribute: params.saveAttribute,\n                    attributeName: params.attributeName,\n                    // If callWith dataAttribute is not specified, the default\n                    // field to check on the record will be .value for values\n                    // coming from another widget than M2M.\n                    callWith: params.callWith || 'value',\n                });\n            }\n        }\n    },\n    /**\n     * @override\n     */\n    async onTargetHide() {\n        await this._super(...arguments);\n        if (this.$target[0].classList.contains('o_snippet_invisible')) {\n            this.$target[0].classList.add('o_conditional_hidden');\n        }\n    },\n    /**\n     * @override\n     */\n    async onTargetShow() {\n        await this._super(...arguments);\n        this.$target[0].classList.remove('o_conditional_hidden');\n    },\n    // Todo: remove me in master.\n    /**\n     * @override\n     */\n    cleanForSave() {},\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Inserts or deletes record's id and value in target's data-attributes\n     * if no ids are selected, deletes the attribute.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectRecord(previewMode, widgetValue, params) {\n        const recordsData = JSON.parse(widgetValue);\n        if (recordsData.length) {\n            this.$target[0].dataset[params.saveAttribute] = widgetValue;\n        } else {\n            delete this.$target[0].dataset[params.saveAttribute];\n        }\n\n        this._updateCSSSelectors();\n    },\n    /**\n     * Selects a value for target's data-attributes.\n     * Should be used instead of selectRecord if the visibility is not related\n     * to database values.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectValue(previewMode, widgetValue, params) {\n        if (widgetValue) {\n            const widgetValueIndex = params.possibleValues.indexOf(widgetValue);\n            const value = [{value: widgetValue, id: widgetValueIndex}];\n            this.$target[0].dataset[params.saveAttribute] = JSON.stringify(value);\n        } else {\n            delete this.$target[0].dataset[params.saveAttribute];\n        }\n\n        this._updateCSSSelectors();\n    },\n    /**\n     * Opens the toggler when 'conditional' is selected.\n     *\n     * @override\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n\n        if (params.attributeName === 'visibility') {\n            const targetEl = this.$target[0];\n            if (widgetValue === 'conditional') {\n                const collapseEl = this.$el.children('we-collapse')[0];\n                this._toggleCollapseEl(collapseEl);\n            } else {\n                // TODO create a param to allow doing this automatically for genericSelectDataAttribute?\n                delete targetEl.dataset.visibility;\n\n                for (const attribute of this.optionsAttributes) {\n                    delete targetEl.dataset[attribute.saveAttribute];\n                    delete targetEl.dataset[`${attribute.saveAttribute}Rule`];\n                }\n            }\n            this.trigger_up('snippet_option_visibility_update', {show: true});\n        } else if (!params.isVisibilityCondition) {\n            return;\n        }\n\n        this._updateCSSSelectors();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName === 'selectRecord') {\n            return this.$target[0].dataset[params.saveAttribute] || '[]';\n        }\n        if (methodName === 'selectValue') {\n            const selectedValue = this.$target[0].dataset[params.saveAttribute];\n            return selectedValue ? JSON.parse(selectedValue)[0].value : params.attributeDefaultValue;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Reads target's attributes and creates CSS selectors.\n     * Stores them in data-attributes to then be reapplied by\n     * content/inject_dom.js (ideally we should saved them in a <style> tag\n     * directly but that would require a new website.page field and would not\n     * be possible in dynamic (controller) pages... maybe some day).\n     *\n     * @private\n     */\n    _updateCSSSelectors() {\n        // There are 2 data attributes per option:\n        // - One that stores the current records selected\n        // - Another that stores the value of the rule \"Hide for / Visible for\"\n        const visibilityIDParts = [];\n        const onlyAttributes = [];\n        const hideAttributes = [];\n        const target = this.$target[0];\n        for (const attribute of this.optionsAttributes) {\n            if (target.dataset[attribute.saveAttribute]) {\n                let records = JSON.parse(target.dataset[attribute.saveAttribute]).map(record => {\n                    return { id: record.id, value: record[attribute.callWith] };\n                });\n                if (attribute.saveAttribute === 'visibilityValueLang') {\n                    records = records.map(lang => {\n                        lang.value = pyToJsLocale(lang.value);\n                        return lang;\n                    });\n                }\n                const hideFor = target.dataset[`${attribute.saveAttribute}Rule`] === 'hide';\n                if (hideFor) {\n                    hideAttributes.push({ name: attribute.attributeName, records: records});\n                } else {\n                    onlyAttributes.push({ name: attribute.attributeName, records: records});\n                }\n                // Create a visibilityId based on the options name and their\n                // values. eg : hide for en_US(id:1) -> lang1h\n                const type = attribute.attributeName.replace('data-', '');\n                const valueIDs = records.map(record => record.id).sort();\n                visibilityIDParts.push(`${type}_${hideFor ? 'h' : 'o'}_${valueIDs.join('_')}`);\n            }\n        }\n        const visibilityId = visibilityIDParts.join('_');\n        // Creates CSS selectors based on those attributes, the reducers\n        // combine the attributes' values.\n        let selectors = '';\n        for (const attribute of onlyAttributes) {\n            // e.g of selector:\n            // html:not([data-attr-1=\"valueAttr1\"]):not([data-attr-1=\"valueAttr2\"]) [data-visibility-id=\"ruleId\"]\n            const selector = attribute.records.reduce((acc, record) => {\n                return acc += `:not([${attribute.name}=\"${record.value}\"])`;\n            }, 'html') + ` body:not(.editor_enable) [data-visibility-id=\"${visibilityId}\"]`;\n            selectors += selector + ', ';\n        }\n        for (const attribute of hideAttributes) {\n            // html[data-attr-1=\"valueAttr1\"] [data-visibility-id=\"ruleId\"],\n            // html[data-attr-1=\"valueAttr2\"] [data-visibility-id=\"ruleId\"]\n            const selector = attribute.records.reduce((acc, record, i, a) => {\n                acc += `html[${attribute.name}=\"${record.value}\"] body:not(.editor_enable) [data-visibility-id=\"${visibilityId}\"]`;\n                return acc + (i !== a.length - 1 ? ',' : '');\n            }, '');\n            selectors += selector + ', ';\n        }\n        selectors = selectors.slice(0, -2);\n        if (selectors) {\n            this.$target[0].dataset.visibilitySelectors = selectors;\n        } else {\n            delete this.$target[0].dataset.visibilitySelectors;\n        }\n\n        if (visibilityId) {\n            this.$target[0].dataset.visibilityId = visibilityId;\n        } else {\n            delete this.$target[0].dataset.visibilityId;\n        }\n    },\n});\n\noptions.registry.WebsiteAnimate = options.Class.extend({\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        // Animations for which the \"On Scroll\" and \"Direction\" options are not\n        // available.\n        this.limitedAnimations = ['o_anim_flash', 'o_anim_pulse', 'o_anim_shake', 'o_anim_tada', 'o_anim_flip_in_x', 'o_anim_flip_in_y'];\n        this.isAnimatedText = this.$target.hasClass('o_animated_text');\n        this.$optionsSection = this.$overlay.data('$optionsSection');\n        this.$scrollingElement = $().getScrollingElement(this.ownerDocument);\n        this.$overlay[0].querySelector(\".o_handles\").classList.toggle(\"pe-none\", this.isAnimatedText);\n    },\n    /**\n     * @override\n     */\n    async onBuilt() {\n        this.$target[0].classList.toggle('o_animate_preview', this.$target[0].classList.contains('o_animate'));\n    },\n    /**\n     * @override\n     */\n    onFocus() {\n        if (this.isAnimatedText) {\n            // For animated text, the animation options must be in the editor\n            // toolbar.\n            this.options.wysiwyg.toolbarEl.append(this.$el[0]);\n            this.$optionsSection.addClass('d-none');\n        }\n    },\n    /**\n     * @override\n     */\n    onBlur() {\n        if (this.isAnimatedText) {\n            // For animated text, the options must be returned to their\n            // original location as they were moved in the toolbar.\n            this.$optionsSection.append(this.$el);\n        }\n    },\n    /**\n     * @override\n     */\n    cleanForSave() {\n        if (this.$target[0].closest('.o_animate')) {\n            // As images may have been added in an animated element, we must\n            // remove the lazy loading on them.\n            this._toggleImagesLazyLoading(false);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async selectClass(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (params.forceAnimation && params.name !== 'o_anim_no_effect_opt' && previewMode !== 'reset') {\n            this._forceAnimation();\n        }\n        if (params.isAnimationTypeSelection) {\n            this.$target[0].classList.toggle(\"o_animate_preview\", this.$target[0].classList.contains(\"o_animate\"));\n        }\n    },\n    /**\n     * @override\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (params.forceAnimation) {\n            this._forceAnimation();\n        }\n    },\n    /**\n     * Sets the animation mode.\n     *\n     * @see this.selectClass for parameters\n     */\n    animationMode(previewMode, widgetValue, params) {\n        const targetClassList = this.$target[0].classList;\n        this.$scrollingElement[0].classList.remove('o_wanim_overflow_xy_hidden');\n        targetClassList.remove('o_animating', 'o_animate_both_scroll', 'o_visible', 'o_animated', 'o_animate_out');\n        this.$target[0].style.animationDelay = '';\n        this.$target[0].style.animationPlayState = '';\n        this.$target[0].style.animationName = '';\n        this.$target[0].style.visibility = '';\n        if (widgetValue === 'onScroll') {\n            this.$target[0].dataset.scrollZoneStart = 0;\n            this.$target[0].dataset.scrollZoneEnd = 100;\n        } else {\n            delete this.$target[0].dataset.scrollZoneStart;\n            delete this.$target[0].dataset.scrollZoneEnd;\n        }\n        if (params.activeValue === \"o_animate_on_hover\") {\n            this.trigger_up(\"option_update\", {\n                optionName: \"ImageTools\",\n                name: \"disable_hover_effect\",\n            });\n        }\n        if ((!params.activeValue || params.activeValue === \"o_animate_on_hover\")\n                && widgetValue && widgetValue !== \"onHover\") {\n            // If \"Animation\" was on \"None\" or \"o_animate_on_hover\" and it is no\n            // longer, it is set to \"fade_in\" by default.\n            targetClassList.add('o_anim_fade_in');\n            this._toggleImagesLazyLoading(false);\n        }\n        if (!widgetValue || widgetValue === \"onHover\") {\n            const possibleEffects = this._requestUserValueWidgets('animation_effect_opt')[0].getMethodsParams('selectClass').possibleValues;\n            const possibleDirections = this._requestUserValueWidgets('animation_direction_opt')[0].getMethodsParams('selectClass').possibleValues;\n            const possibleEffectsAndDirections = possibleEffects.concat(possibleDirections);\n            // Remove the classes added by \"Effect\" and \"Direction\" options if\n            // \"Animation\" is \"None\".\n            for (const targetClass of targetClassList.value.split(/\\s+/g)) {\n                if (possibleEffectsAndDirections.indexOf(targetClass) >= 0) {\n                    targetClassList.remove(targetClass);\n                }\n            }\n            this.$target[0].style.setProperty('--wanim-intensity', '');\n            this.$target[0].style.animationDuration = '';\n            this._toggleImagesLazyLoading(true);\n        }\n        if (widgetValue === \"onHover\") {\n            // Pause the history until the hover effect is applied in\n            // \"setImgShapeHoverEffect\". This prevents saving the intermediate\n            // steps done (in a tricky way) up to that point.\n            this.options.wysiwyg.odooEditor.historyPauseSteps();\n            this.trigger_up(\"option_update\", {\n                optionName: \"ImageTools\",\n                name: \"enable_hover_effect\",\n            });\n        }\n    },\n    /**\n     * Sets the animation intensity.\n     *\n     * @see this.selectClass for parameters\n     */\n    animationIntensity(previewMode, widgetValue, params) {\n        this.$target[0].style.setProperty('--wanim-intensity', widgetValue);\n        this._forceAnimation();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    async _forceAnimation() {\n        this.$target.css('animation-name', 'dummy');\n\n        if (this.$target[0].classList.contains('o_animate_on_scroll')) {\n            // Trigger a DOM reflow.\n            void this.$target[0].offsetWidth;\n            this.$target.css('animation-name', '');\n            this.ownerDocument.defaultView.dispatchEvent(new Event('resize'));\n        } else {\n            // Trigger a DOM reflow (Needed to prevent the animation from\n            // being launched twice when previewing the \"Intensity\" option).\n            await new Promise(resolve => setTimeout(resolve));\n            this.$target.addClass('o_animating');\n            this.trigger_up('cover_update', {\n                overlayVisible: true,\n            });\n            this.$scrollingElement[0].classList.add('o_wanim_overflow_xy_hidden');\n            this.$target.css('animation-name', '');\n            this.$target.one('webkitAnimationEnd oanimationend msAnimationEnd animationend', () => {\n                this.$scrollingElement[0].classList.remove('o_wanim_overflow_xy_hidden');\n                this.$target.removeClass('o_animating');\n            });\n        }\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        const hasAnimateClass = this.$target[0].classList.contains(\"o_animate\");\n        switch (widgetName) {\n            case 'no_animation_opt': {\n                return !this.isAnimatedText;\n            }\n            case 'animation_effect_opt': {\n                return hasAnimateClass;\n            }\n            case 'animation_trigger_opt': {\n                return !this.$target[0].closest('.dropdown');\n            }\n            case 'animation_on_scroll_opt':\n            case 'animation_direction_opt': {\n                if (widgetName === \"animation_direction_opt\" && !hasAnimateClass) {\n                    return false;\n                }\n                return !this.limitedAnimations.some(className => this.$target[0].classList.contains(className));\n            }\n            case 'animation_intensity_opt': {\n                if (!hasAnimateClass) {\n                    return false;\n                }\n                const possibleDirections = this._requestUserValueWidgets('animation_direction_opt')[0].getMethodsParams('selectClass').possibleValues;\n                if (this.$target[0].classList.contains('o_anim_fade_in')) {\n                    for (const targetClass of this.$target[0].classList) {\n                        // Show \"Intensity\" if \"Fade in\" + direction is not\n                        // \"In Place\" ...\n                        if (possibleDirections.indexOf(targetClass) >= 0) {\n                            return true;\n                        }\n                    }\n                    // ... but hide if \"Fade in\" + \"In Place\" direction.\n                    return false;\n                }\n                return true;\n            }\n            case 'animation_on_hover_opt': {\n                const [hoverEffectOverlayWidget] = this._requestUserValueWidgets(\"hover_effect_overlay_opt\");\n                if (hoverEffectOverlayWidget) {\n                    const hoverEffectWidget = hoverEffectOverlayWidget.getParent();\n                    const imageToolsOpt = hoverEffectWidget.getParent();\n                    return (\n                        imageToolsOpt._canHaveHoverEffect()\n                        && !await weUtils.isImageCorsProtected(this.$target[0])\n                    );\n                }\n                return false;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeVisibility(methodName, params) {\n        if (this.$target[0].matches('img')) {\n            return isImageSupportedForStyle(this.$target[0]);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'animationIntensity') {\n            return window.getComputedStyle(this.$target[0]).getPropertyValue('--wanim-intensity');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Removes or adds the lazy loading on images because animated images can\n     * appear before or after their parents and cause bugs in the animations.\n     * To put \"lazy\" back on the \"loading\" attribute, we simply remove the\n     * attribute as it is automatically added on page load.\n     *\n     * @private\n     * @param {Boolean} lazy\n     */\n    _toggleImagesLazyLoading(lazy) {\n        const imgEls = this.$target[0].matches('img')\n            ? [this.$target[0]]\n            : this.$target[0].querySelectorAll('img');\n        for (const imgEl of imgEls) {\n            if (lazy) {\n                // Let the automatic system add the loading attribute\n                imgEl.removeAttribute('loading');\n            } else {\n                imgEl.loading = 'eager';\n            }\n        }\n    },\n});\n\n/**\n * Allows edition of text \"Highlight Effects\" following this generic structure:\n * `<span class=\"o_text_highlight\">\n *      <span class=\"o_text_highlight_item\">\n *          line1-textNode1 [line1-textNode2,...]\n *          <svg.../>\n *      </span>\n *      [<br/>]\n *      <span class=\"o_text_highlight_item\">\n *          line2-textNode1 [line2-textNode2,...]\n *          <svg.../>\n *      </span>\n *      ...\n * </span>`\n * To correctly adapt each highlight unit when the text content is changed.\n */\noptions.registry.TextHighlight = options.Class.extend({\n    custom_events: Object.assign({}, options.Class.prototype.custom_events, {\n        \"user_value_widget_opening\": \"_onWidgetOpening\",\n    }),\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.leftPanelEl = this.$overlay.data(\"$optionsSection\")[0];\n        // Reduce overlay opacity for more highlight visibility on small text.\n        this.$overlay[0].style.opacity = \"0.25\";\n        this.$overlay[0].querySelector(\".o_handles\").classList.add(\"pe-none\");\n    },\n    /**\n     * Move \"Text Effect\" options to the editor's toolbar.\n     *\n     * @override\n     */\n    onFocus() {\n        this.options.wysiwyg.toolbarEl.append(this.$el[0]);\n    },\n    /**\n     * @override\n     */\n    onBlur() {\n        this.leftPanelEl.appendChild(this.el);\n    },\n    /**\n    * @override\n    */\n    notify(name, data) {\n        // Apply the highlight effect DOM structure when added for the first time\n        // and display the highlight effects grid immediately.\n        if (name === \"new_text_highlight\") {\n            this._autoAdaptHighlights();\n            this._requestUserValueWidgets(\"text_highlight_opt\")[0]?.enable();\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Activates & deactivates the text highlight effect.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setTextHighlight(previewMode, widgetValue, params) {\n        return widgetValue ? this._addTextHighlight(widgetValue)\n            : removeTextHighlight(this.$target[0]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Used to add a highlight SVG element to the targeted text node(s).\n     * This should also take in consideration a situation where many text nodes\n     * are separate e.g. `<p>first text content<br/>second text content...</p>`.\n     * To correctly handle those situations, every set of text nodes will be\n     * wrapped in a `.o_text_highlight_item` that contains its highlight SVG.\n     *\n     * @param {String} highlightID\n     * @private\n     */\n    _addTextHighlight(highlightID) {\n        const highlightEls = [...this.$target[0].querySelectorAll(\".o_text_highlight_item svg\")];\n        if (highlightEls.length) {\n            // If the text element has a highlight effect, we only need to\n            // change the SVG.\n            highlightEls.forEach(svg => {\n                svg.after(drawTextHighlightSVG(svg.parentElement, highlightID));\n                svg.remove();\n            });\n        } else {\n            this._autoAdaptHighlights();\n        }\n    },\n    /**\n     * Used to set the highlight effect DOM structure on the targeted text\n     * content.\n     *\n     * @private\n     */\n    _autoAdaptHighlights() {\n        this.trigger_up(\"snippet_edition_request\", { exec: async () =>\n            await this._refreshPublicWidgets($(this.options.wysiwyg.odooEditor.editable))\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * To draw highlight SVGs for `<we-select/>` preview, we need to open the\n     * widget (we need correct size values from `getBoundingClientRect()`).\n     * This code will build the highlight preview the first time we open the\n     * `<we-select/>`.\n     *\n     * @private\n     */\n    _onWidgetOpening(ev) {\n        const target = ev.target;\n        // Only when there is no highlight SVGs.\n        if (target.getName() === \"text_highlight_opt\" && !target.el.querySelector(\"svg\")) {\n            const weToggler = target.el.querySelector(\"we-toggler\");\n            weToggler.classList.add(\"active\");\n            [...target.el.querySelectorAll(\"we-button[data-set-text-highlight] div\")].forEach(weBtnEl => {\n                weBtnEl.textContent = \"Text\";\n                // Get the text highlight linked to each `<we-button/>`\n                // and apply it to its text content.\n                weBtnEl.append(drawTextHighlightSVG(weBtnEl, weBtnEl.parentElement.dataset.setTextHighlight));\n            });\n        }\n    },\n});\n\n/**\n * Replaces current target with the specified template layout\n */\noptions.registry.MegaMenuLayout = options.registry.SelectTemplate.extend({\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.selectTemplateWidgetName = 'mega_menu_template_opt';\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    notify(name, data) {\n        if (name === 'reset_template') {\n            const xmlid = this._getCurrentTemplateXMLID();\n            this._getTemplate(xmlid).then(template => {\n                this.containerEl.insertAdjacentHTML('beforeend', template);\n                data.onSuccess();\n            });\n        } else {\n            this._super(...arguments);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'selectTemplate') {\n            return this._getCurrentTemplateXMLID();\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     * @returns {string} xmlid of the current template.\n     */\n    _getCurrentTemplateXMLID: function () {\n        const templateDefiningClass = this.containerEl.querySelector('section')\n            .classList.value.split(' ').filter(cl => cl.startsWith('s_mega_menu'))[0];\n        return `website.${templateDefiningClass}`;\n    },\n});\n\n/**\n * Hides delete and clone buttons for Mega Menu block.\n */\noptions.registry.MegaMenuNoDelete = options.Class.extend({\n    forceNoDeleteButton: true,\n\n    /**\n     * @override\n     */\n    async onRemove() {\n        await new Promise(resolve => {\n            this.trigger_up('option_update', {\n                optionName: 'MegaMenuLayout',\n                name: 'reset_template',\n                data: {\n                    onSuccess: () => resolve(),\n                }\n            });\n        });\n    },\n});\n\noptions.registry.sizing.include({\n    /**\n     * @override\n     */\n    start() {\n        const defs = this._super(...arguments);\n        const self = this;\n        this.$handles.on('mousedown', function (ev) {\n            // Since website is edited in an iframe, a div that goes over the\n            // iframe is necessary to catch mousemove and mouseup events,\n            // otherwise the iframe absorbs them.\n            const $body = $(this.ownerDocument.body);\n            if (!self.divEl) {\n                self.divEl = document.createElement('div');\n                self.divEl.style.position = 'absolute';\n                self.divEl.style.height = '100%';\n                self.divEl.style.width = '100%';\n                self.divEl.setAttribute('id', 'iframeEventOverlay');\n                $body.append(self.divEl);\n            }\n            const documentMouseUp = () => {\n                // Multiple mouseup can occur if mouse goes out of the window\n                // while moving.\n                if (self.divEl) {\n                    self.divEl.remove();\n                    self.divEl = undefined;\n                }\n                $body.off('mouseup', documentMouseUp);\n            };\n            $body.on('mouseup', documentMouseUp);\n        });\n        return defs;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUIVisibility() {\n        await this._super(...arguments);\n        const nonDraggableClasses = [\n            's_table_of_content_navbar_wrap',\n            's_table_of_content_main',\n        ];\n        if (nonDraggableClasses.some(c => this.$target[0].classList.contains(c))) {\n            const moveHandleEl = this.$overlay[0].querySelector('.o_move_handle');\n            moveHandleEl.classList.add('d-none');\n        }\n    },\n});\n\noptions.registry.SwitchableViews = options.Class.extend({\n    /**\n     * @override\n     */\n    async willStart() {\n        const _super = this._super.bind(this);\n        this.switchableRelatedViews = await new Promise((resolve, reject) => {\n            this.trigger_up('get_switchable_related_views', {\n                onSuccess: resolve,\n                onFailure: reject,\n            });\n        });\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        for (const view of this.switchableRelatedViews) {\n            const weCheckboxEl = document.createElement('we-checkbox');\n            weCheckboxEl.setAttribute('string', view.name);\n            weCheckboxEl.setAttribute('data-customize-website-views', view.key);\n            weCheckboxEl.setAttribute('data-no-preview', 'true');\n            weCheckboxEl.setAttribute('data-reload', '/');\n            uiFragment.appendChild(weCheckboxEl);\n        }\n    },\n    /***\n     * @override\n     */\n    _computeVisibility() {\n        return !!this.switchableRelatedViews.length;\n    },\n    /**\n     * @override\n     */\n    _checkIfWidgetsUpdateNeedReload() {\n        return true;\n    }\n});\n\noptions.registry.GridImage = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    changeGridImageMode(previewMode, widgetValue, params) {\n        const imageGridItemEl = this._getImageGridItem();\n        if (imageGridItemEl) {\n            imageGridItemEl.classList.toggle('o_grid_item_image_contain', widgetValue === 'contain');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns the parent column if it is marked as a grid item containing an\n     * image.\n     *\n     * @returns {?HTMLElement}\n     */\n    _getImageGridItem() {\n        return this.$target[0].closest(\".o_grid_item_image\");\n    },\n    /**\n     * @override\n     */\n    _computeVisibility() {\n        // Special conditions for the hover effects.\n        const hasSquareShape = this.$target[0].dataset.shape === \"web_editor/geometric/geo_square\";\n        const effectAllowsOption = ![\"dolly_zoom\", \"outline\", \"image_mirror_blur\"]\n            .includes(this.$target[0].dataset.hoverEffect);\n\n        return this._super(...arguments)\n            && !!this._getImageGridItem()\n            && (!('shape' in this.$target[0].dataset)\n                || hasSquareShape && effectAllowsOption);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'changeGridImageMode') {\n            const imageGridItemEl = this._getImageGridItem();\n            return imageGridItemEl && imageGridItemEl.classList.contains('o_grid_item_image_contain')\n                ? 'contain'\n                : 'cover';\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.GalleryElement = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Allows to change the position of an item on the set.\n     *\n     * @see this.selectClass for parameters\n     */\n    position(previewMode, widgetValue, params) {\n        const carouselOptionName = this.$target[0].parentNode.parentNode.classList.contains(\"s_carousel_intro\") ? \"CarouselIntro\" : \"Carousel\";\n        const optionName = this.$target[0].classList.contains(\"carousel-item\") ? carouselOptionName\n            : \"GalleryImageList\";\n        const itemEl = this.$target[0];\n        this.trigger_up(\"option_update\", {\n            optionName: optionName,\n            name: \"reorder_items\",\n            data: {\n                itemEl: itemEl,\n                position: widgetValue,\n            },\n        });\n    },\n});\n\noptions.registry.Button = options.Class.extend({\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        const isUnremovableButton = this.$target[0].classList.contains(\"oe_unremovable\");\n        this.forceDuplicateButton = !isUnremovableButton;\n        this.forceNoDeleteButton = isUnremovableButton;\n    },\n    /**\n     * @override\n     */\n    onBuilt(options) {\n        // Only if the button is built, not if a snippet containing that button\n        // is built (e.g. true if dropping a button from the snippet menu onto\n        // the page, false if dropping an \"image-text\" snippet).\n        if (options.isCurrent) {\n            this._adaptButtons();\n        }\n    },\n    /**\n     * @override\n     */\n    onClone(options) {\n        // Only if the button is cloned, not if a snippet containing that button\n        // is cloned.\n        if (options.isCurrent) {\n            this._adaptButtons(false);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Checks if there are buttons before or after the target element and\n     * applies appropriate styling.\n     *\n     * @private\n     * @param {Boolean} [adaptAppearance=true]\n     */\n    _adaptButtons(adaptAppearance = true) {\n        const previousSiblingEl = this.$target[0].previousElementSibling;\n        const nextSiblingEl = this.$target[0].nextElementSibling;\n        let siblingButtonEl = null;\n        // When multiple buttons follow each other, they may break on 2 lines or\n        // more on mobile, so they need a margin-bottom. Also, if the button is\n        // dropped next to another button add a space between them.\n        if (nextSiblingEl?.matches(\".btn\")) {\n            nextSiblingEl.classList.add(\"mb-2\");\n            this.$target[0].after(' ');\n            // It is first the next button that we put in this variable because\n            // we want to copy as a priority the style of the previous button\n            // if it exists.\n            siblingButtonEl = nextSiblingEl;\n        }\n        if (previousSiblingEl?.matches(\".btn\")) {\n            previousSiblingEl.classList.add(\"mb-2\");\n            this.$target[0].before(' ');\n            siblingButtonEl = previousSiblingEl;\n        }\n        if (siblingButtonEl) {\n            this.$target[0].classList.add(\"mb-2\");\n        }\n        if (adaptAppearance) {\n            if (siblingButtonEl && !this.$target[0].matches(\".s_custom_button\")) {\n                // If the dropped button is not a custom button then we adjust\n                // its appearance to match its sibling.\n                if (siblingButtonEl.classList.contains(\"btn-secondary\")) {\n                    this.$target[0].classList.remove(\"btn-primary\");\n                    this.$target[0].classList.add(\"btn-secondary\");\n                }\n                if (siblingButtonEl.classList.contains(\"btn-sm\")) {\n                    this.$target[0].classList.add(\"btn-sm\");\n                } else if (siblingButtonEl.classList.contains(\"btn-lg\")) {\n                    this.$target[0].classList.add(\"btn-lg\");\n                }\n            } else {\n                // To align with the editor's behavior, we need to enclose the\n                // button in a <p> tag if it's not dropped within a <p> tag. We only\n                // put the dropped button in a <p> if it's not next to another\n                // button, because some snippets have buttons that aren't inside a\n                // <p> (e.g. s_text_cover).\n                // TODO: this definitely needs to be fixed at web_editor level.\n                // Nothing should prevent adding buttons outside of a paragraph.\n                const btnContainerEl = this.$target[0].closest(\"p\");\n                if (!btnContainerEl) {\n                    const paragraphEl = document.createElement(\"p\");\n                    this.$target[0].parentNode.insertBefore(paragraphEl, this.$target[0]);\n                    paragraphEl.appendChild(this.$target[0]);\n                }\n            }\n            this.$target[0].classList.remove(\"s_custom_button\");\n        }\n    },\n});\n\noptions.registry.layout_column.include({\n    /**\n     * @override\n     */\n    _isMobile() {\n        return wUtils.isMobile(this);\n    },\n});\n\noptions.registry.SnippetMove.include({\n    /**\n     * @override\n     */\n    _isMobile() {\n        return wUtils.isMobile(this);\n    },\n});\n\nexport default {\n    UrlPickerUserValueWidget: UrlPickerUserValueWidget,\n    FontFamilyPickerUserValueWidget: FontFamilyPickerUserValueWidget,\n};\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.Pricelist = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Show/hide descriptions.\n     */\n    toggleDescription(previewMode, widgetValue, params) {\n        const dishes = this.$target[0].querySelectorAll(\".\" + params.itemsClass);\n        let description;\n\n        if (widgetValue) {\n            dishes.forEach((el) => {\n                description = el.querySelector(\".\" + params.descriptionClass);\n                if (description) {\n                    description.classList.remove(\"d-none\");\n                } else {\n                    const descriptionEl = document.createElement(\"p\");\n                    descriptionEl.classList.add(params.descriptionClass, \"d-block\", \"pe-5\", \"text-muted\", \"o_default_snippet_text\");\n                    descriptionEl.textContent = _t(\"Add a description here\");\n                    el.appendChild(descriptionEl);\n                }\n            });\n        } else {\n            dishes.forEach((el) => {\n                description = el.querySelector(\".\" + params.descriptionClass);\n                if (description && (description.classList.contains(\"o_default_snippet_text\") || description.querySelector(\".o_default_snippet_text\"))) {\n                    description.remove();\n                } else if (description) {\n                    description.classList.add(\"d-none\");\n                }\n            });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === \"toggleDescription\") {\n            const description = this.$target[0].querySelector(\".\" + params.descriptionClass);\n            return description && !description.classList.contains(\"d-none\");\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { pick } from \"@web/core/utils/objects\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.facebookPage = options.Class.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.notification = this.bindService(\"notification\");\n    },\n\n    /**\n     * Initializes the required facebook page data to create the iframe.\n     *\n     * @override\n     */\n    willStart: function () {\n        var defs = [this._super.apply(this, arguments)];\n\n        var defaults = {\n            href: '',\n            id: '',\n            height: 215,\n            width: 350,\n            tabs: '',\n            small_header: true,\n            hide_cover: \"true\",\n        };\n        this.fbData = Object.assign({}, defaults, pick(this.$target[0].dataset, ...Object.keys(defaults)));\n        if (!this.fbData.href) {\n            // Fetches the default url for facebook page from website config\n            var self = this;\n            defs.push(this.orm.searchRead(\"website\", [], [\"social_facebook\"], {\n                limit: 1,\n            }).then(function (res) {\n                if (res) {\n                    self.fbData.href = res[0].social_facebook || '';\n                }\n            }));\n        }\n\n        return Promise.all(defs).then(() => this._markFbElement()).then(() => this._refreshPublicWidgets());\n    },\n    /**\n     * @override\n     */\n    onBuilt() {\n        this.$target[0].querySelector('.o_facebook_page_preview')?.remove();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Toggles a checkbox option.\n     *\n     * @see this.selectClass for parameters\n     * @param {String} optionName the name of the option to toggle\n     */\n    toggleOption: function (previewMode, widgetValue, params) {\n        let optionName = params.optionName;\n        if (optionName.startsWith('tab.')) {\n            optionName = optionName.replace('tab.', '');\n            if (widgetValue) {\n                this.fbData.tabs = this.fbData.tabs\n                    .split(',')\n                    .filter(t => t !== '')\n                    .concat([optionName])\n                    .join(',');\n            } else {\n                this.fbData.tabs = this.fbData.tabs\n                    .split(',')\n                    .filter(t => t !== optionName)\n                    .join(',');\n            }\n        } else {\n            if (optionName === 'show_cover') {\n                this.fbData.hide_cover = widgetValue ? \"false\" : \"true\";\n            } else {\n                this.fbData[optionName] = widgetValue;\n            }\n        }\n        return this._markFbElement();\n    },\n    /**\n     * Sets the facebook page's URL.\n     *\n     * @see this.selectClass for parameters\n     */\n    pageUrl: function (previewMode, widgetValue, params) {\n        this.fbData.href = widgetValue;\n        return this._markFbElement();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        const alertEl = document.createElement(\"we-alert\");\n        const titleEl = document.createElement(\"we-title\");\n        titleEl.textContent = _t(\"Recent Facebook Issues\");\n        const descEl = document.createElement(\"span\");\n        descEl.textContent = _t(\"This block will temporarily not be shown on mobile due to recent Facebook issues.\");\n        alertEl.appendChild(titleEl);\n        alertEl.appendChild(descEl);\n        uiFragment.prepend(alertEl);\n        return this._super(...arguments);\n    },\n    /**\n     * Sets the correct dataAttributes on the facebook iframe and refreshes it.\n     *\n     * @see this.selectClass for parameters\n     */\n    _markFbElement: function () {\n        return this._checkURL().then(() => {\n            // Managing height based on options\n            if (this.fbData.tabs) {\n                this.fbData.height = this.fbData.tabs === 'events' ? 300 : 500;\n            } else if (this.fbData.small_header) {\n                this.fbData.height = 70;\n            } else {\n                this.fbData.height = 150;\n            }\n            for (const [key, value] of Object.entries(this.fbData)) {\n                this.$target[0].dataset[key] = value;\n            }\n        });\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        const optionName = params.optionName;\n        switch (methodName) {\n            case 'toggleOption': {\n                if (optionName.startsWith('tab.')) {\n                    return this.fbData.tabs.split(',').includes(optionName.replace(/^tab./, ''));\n                } else {\n                    if (optionName === 'show_cover') {\n                        return this.fbData.hide_cover === \"false\";\n                    }\n                    return this.fbData[optionName];\n                }\n            }\n            case 'pageUrl': {\n                return this._checkURL().then(() => this.fbData.href);\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _checkURL: function () {\n        const defaultURL = 'https://www.facebook.com/Odoo';\n        // Patterns matched by the regex (all relate to existing pages,\n        // in spite of the URLs containing \"profile.php\" or \"people\"):\n        // - https://www.facebook.com/<pagewithaname>\n        // - http://www.facebook.com/<page.with.a.name>\n        // - www.facebook.com/<fbid>\n        // - facebook.com/profile.php?id=<fbid>\n        // - www.facebook.com/<name>-<fbid>  - NB: the name doesn't matter\n        // - www.fb.com/people/<name>/<fbid>  - same\n        // - m.facebook.com/p/<name>-<fbid>  - same\n        // The regex is kept as a huge one-liner for performance as it is\n        // compiled once on script load. The only way to split it on several\n        // lines is with the RegExp constructor, which is compiled on runtime.\n        const match = this.fbData.href.trim().match(/^(https?:\\/\\/)?((www\\.)?(fb|facebook)|(m\\.)?facebook)\\.com\\/(((profile\\.php\\?id=|people\\/([^/?#]+\\/)?|(p\\/)?[^/?#]+-)(?<id>[0-9]{12,16}))|(?<nameid>[\\w.]+))($|[/?# ])/);\n        if (match) {\n            // Check if the page exists on Facebook or not\n            const pageId = match.groups.nameid || match.groups.id;\n            return fetch(`https://graph.facebook.com/${pageId}/picture`)\n            .then((res) => {\n                if (res.ok) {\n                    this.fbData.id = pageId;\n                } else {\n                    this.fbData.id = \"\";\n                    this.fbData.href = defaultURL;\n                    this.notification.add(_t(\"We couldn't find the Facebook page\"), {\n                        type: \"warning\",\n                    });\n                }\n            });\n        }\n        this.fbData.id = \"\";\n        this.fbData.href = defaultURL;\n        this.notification.add(_t(\"You didn't provide a valid Facebook link\"), {\n            type: \"warning\",\n        });\n        return Promise.resolve();\n    },\n});\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport { MediaDialog } from \"@web_editor/components/media_dialog/media_dialog\";\n\noptions.registry.ImageSnippet = options.Class.extend({\n    /**\n     * @override\n     */\n    async onBuilt() {\n        // When the placeholder has been dropped we directly open the media\n        // dialog.\n        await new Promise(resolve => {\n            let isImageSaved = false;\n            this.call(\"dialog\", \"add\", MediaDialog, {\n                onlyImages: true,\n                save: imageEl => {\n                    isImageSaved = true;\n                    // Replace the placeholder with the new image.\n                    this.$target[0].parentNode.insertBefore(imageEl, this.$target[0]);\n                    this.$target[0].parentNode.removeChild(this.$target[0]);\n                },\n            }, {\n                onClose: () => {\n                    if (!isImageSaved) {\n                        // Revert the current step to exclude the step where the\n                        // placeholder is added and then removed from the DOM\n                        this.options.wysiwyg.odooEditor.historyRevertCurrentStep();\n                        // If no image has been chosen, the placeholder is\n                        // removed.\n                        this.$target[0].remove();\n                    }\n                    resolve();\n                }\n            });\n        });\n    },\n});\n\nexport default {\n    ImageSnippet: options.registry.ImageSnippet,\n};\n", "/** @odoo-module **/\n\nimport { MediaDialog } from \"@web_editor/components/media_dialog/media_dialog\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport wUtils from '@website/js/utils';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport {\n    loadImageInfo,\n    applyModifications,\n} from \"@web_editor/js/editor/image_processing\";\n\n/**\n * This class provides layout methods for interacting with the ImageGallery\n * snippet. It is used by all options that need the layout to be recomputed.\n * This is typically the case when adding/removing/moving images, changing the\n * layout mode and changing the number of columns.\n */\noptions.registry.GalleryLayout = options.registry.CarouselHandler.extend({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Get the image target's layout mode (slideshow, masonry, grid or nomode).\n     *\n     * @private\n     * @returns {String('slideshow'|'masonry'|'grid'|'nomode')}\n     */\n    _getMode() {\n        var mode = 'slideshow';\n        if (this.$target.hasClass('o_masonry')) {\n            mode = 'masonry';\n        }\n        if (this.$target.hasClass('o_grid')) {\n            mode = 'grid';\n        }\n        if (this.$target.hasClass('o_nomode')) {\n            mode = 'nomode';\n        }\n        return mode;\n    },\n    /**\n     * Displays the images with the \"grid\" layout.\n     *\n     * @private\n     */\n    _grid() {\n        const imgs = this._getImgHolderEls();\n        var $row = $('<div/>', {class: 'row s_nb_column_fixed'});\n        var columns = this._getColumns();\n        var colClass = 'col-lg-' + (12 / columns);\n        var $container = this._replaceContent($row);\n\n        imgs.forEach((img, index) => {\n            const $img = $(img.cloneNode(true));\n            var $col = $('<div/>', {class: colClass});\n            $col.append($img).appendTo($row);\n            if ((index + 1) % columns === 0) {\n                $row = $('<div/>', {class: 'row s_nb_column_fixed'});\n                $row.appendTo($container);\n            }\n        });\n        this.$target.css('height', '');\n    },\n    /**\n     * Displays the images with the \"masonry\" layout.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    _masonry() {\n        const imgs = this._getImgHolderEls();\n        var columns = this._getColumns();\n        var colClass = 'col-lg-' + (12 / columns);\n        var cols = [];\n\n        var $row = $('<div/>', {class: 'row s_nb_column_fixed'});\n        this._replaceContent($row);\n\n        // Create columns\n        for (var c = 0; c < columns; c++) {\n            var $col = $('<div/>', {class: 'o_masonry_col o_snippet_not_selectable ' + colClass});\n            $row.append($col);\n            cols.push($col[0]);\n        }\n\n        // Dispatch images in columns by always putting the next one in the\n        // smallest-height column\n        return new Promise(async resolve => {\n            for (const imgEl of imgs) {\n                let min = Infinity;\n                let smallestColEl;\n                for (const colEl of cols) {\n                    const imgEls = colEl.querySelectorAll(\"img\");\n                    const lastImgRect = imgEls.length && imgEls[imgEls.length - 1].getBoundingClientRect();\n                    const height = lastImgRect ? Math.round(lastImgRect.top + lastImgRect.height) : 0;\n                    if (height < min) {\n                        min = height;\n                        smallestColEl = colEl;\n                    }\n                }\n                // Only on Chrome: appended images are sometimes invisible\n                // and not correctly loaded from cache, we use a clone of the\n                // image to force the loading.\n                smallestColEl.append(imgEl.cloneNode(true));\n                await wUtils.onceAllImagesLoaded(this.$target);\n            }\n            resolve();\n        });\n    },\n    /**\n     * Allows to change the images layout. @see grid, masonry, nomode, slideshow\n     *\n     * @private\n     * @param {string} modeName\n     * @returns {Promise}\n     */\n    async _setMode(modeName) {\n        modeName = modeName || 'slideshow'; // FIXME should not be needed\n        this.$target.css('height', '');\n        this.$target\n            .removeClass('o_nomode o_masonry o_grid o_slideshow')\n            .addClass('o_' + modeName);\n        // Used to prevent the editor's \"unbreakable protection mechanism\" from\n        // restoring Image Wall adaptations (images removed > new images added\n        // to the container & layout updates) when adding new images to the\n        // snippet.\n        if (this.options.wysiwyg) {\n            this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n        }\n        await this[`_${modeName}`]();\n        this.trigger_up('cover_update');\n        await this._refreshPublicWidgets();\n    },\n    /**\n     * Displays the images with the standard layout: floating images.\n     *\n     * @private\n     */\n    _nomode() {\n        var $row = $('<div/>', {class: 'row s_nb_column_fixed'});\n        const imgs = this._getItemsGallery();\n        const imgHolderEls = this._getImgHolderEls();\n\n        this._replaceContent($row);\n\n        imgs.forEach((img, index) => {\n            var wrapClass = 'col-lg-3';\n            if (img.width >= img.height * 2 || img.width > 600) {\n                wrapClass = 'col-lg-6';\n            }\n            var $wrap = $('<div/>', {class: wrapClass}).append(imgHolderEls[index]);\n            $row.append($wrap);\n        });\n    },\n    /**\n     * Displays the images with a \"slideshow\" layout.\n     *\n     * @private\n     */\n    _slideshow() {\n        const imageEls = this._getItemsGallery();\n        const imgHolderEls = this._getImgHolderEls();\n        var currentInterval = this.$target.find('.carousel:first').attr('data-bs-interval');\n        let params = {\n            images: imageEls,\n            index: 0,\n            interval: currentInterval || 0,\n            ride: !currentInterval ? \"false\" : \"carousel\",\n            id: 'slideshow_' + new Date().getTime(),\n            hideImage: true,\n        };\n        // Since there is no versioning for this snippet we use the last version\n        // of \"website.gallery.slideshow\" called \"website.s_image_gallery_mirror\"\n        if (this.$target[0].dataset.vcss === '002') {\n            let carouselEl = this.$target[0].querySelector('.carousel');\n            params.colorContrast  = carouselEl && carouselEl.classList.contains('carousel-dark') ? 'carousel-dark' : ' ';\n        }\n        let $slideshow = $(renderToElement('website.s_image_gallery_mirror', params));\n        const carouselItemEls = $slideshow[0].querySelectorAll(\".carousel-item\");\n        carouselItemEls.forEach((carouselItemEl, index) => {\n            // Add the images in the carousel items.\n            carouselItemEl.appendChild(imgHolderEls[index]);\n        });\n        this._replaceContent($slideshow);\n        this.$(\"img\").toArray().forEach((img, index) => {\n            $(img).attr({contenteditable: true, 'data-index': index});\n        });\n        this.$target.css('height', Math.round(window.innerHeight * 0.7));\n\n        // Apply layout animation\n        this.$target.off('slide.bs.carousel').off('slid.bs.carousel');\n        this._slideshowStart();\n        this.$('li.fa').off('click');\n    },\n    /**\n     * @override\n     */\n    _getItemsGallery() {\n        const imgs = this.$('img').get();\n        imgs.sort((a, b) => this._getIndex(a) - this._getIndex(b));\n        return imgs;\n    },\n    /**\n     * Returns the images, or the images holder if this holder is an anchor,\n     * sorted by index.\n     *\n     * @private\n     * @returns {Array.<HTMLImageElement|HTMLAnchorElement>}\n     */\n    _getImgHolderEls: function () {\n        const imgEls = this._getItemsGallery();\n        return imgEls.map(imgEl => imgEl.closest(\"a\") || imgEl);\n    },\n    /**\n     * Returns the index associated to a given image.\n     *\n     * @private\n     * @param {DOMElement} img\n     * @returns {integer}\n     */\n    _getIndex: function (img) {\n        return img.dataset.index || 0;\n    },\n    /**\n     * Returns the currently selected column option.\n     *\n     * @private\n     * @returns {integer}\n     */\n    _getColumns: function () {\n        return parseInt(this.$target.attr('data-columns')) || 3;\n    },\n    /**\n     * @override\n     */\n    _reorderItems(itemsEls, newItemPosition) {\n        itemsEls.forEach((img, index) => {\n            img.dataset.index = index;\n        });\n        this.trigger_up('snippet_edition_request', {exec: async () => {\n            await this._relayout();\n            if (this._getMode() === \"slideshow\") {\n                this._updateIndicatorAndActivateSnippet(newItemPosition);\n            } else {\n                const imageEl = this.$target[0].querySelector(`[data-index='${newItemPosition}']`);\n                this.trigger_up(\"activate_snippet\", {\n                    $snippet: $(imageEl),\n                    ifInactiveOptions: true,\n                });\n            }\n        }});\n    },\n    /**\n     * Empties the container, adds the given content and returns the container.\n     *\n     * @private\n     * @param {jQuery} $content\n     * @returns {jQuery} the main container of the snippet\n     */\n    _replaceContent: function ($content) {\n        var $container = this.$('> .container, > .container-fluid, > .o_container_small');\n        $container.empty().append($content);\n        return $container;\n    },\n    /**\n     * Redraws the current layout.\n     *\n     * @private\n     */\n    _relayout() {\n        return this._setMode(this._getMode());\n    },\n    /**\n     * Sets up listeners on slideshow to activate selected image.\n     */\n    _slideshowStart() {\n        const $carousel = this.$bsTarget.is(\".carousel\") ? this.$bsTarget : this.$bsTarget.find(\".carousel\");\n        let _previousEditor;\n        let _miniatureClicked;\n        const carouselIndicatorsEl = this.$target[0].querySelector(\".carousel-indicators\");\n        if (carouselIndicatorsEl) {\n            carouselIndicatorsEl.addEventListener(\"click\", () => {\n                _miniatureClicked = true;\n            });\n        }\n        let lastSlideTimeStamp;\n        $carousel.on(\"slide.bs.carousel.image_gallery\", (ev) => {\n            lastSlideTimeStamp = ev.timeStamp;\n            const activeImageEl = this.$target[0].querySelector(\".carousel-item.active img\");\n            this.trigger_up(\"is_element_selected\", {\n                el: activeImageEl,\n                callback: () => {\n                    _previousEditor = true;\n                },\n            });\n            this.trigger_up(\"hide_overlay\");\n        });\n        $carousel.on(\"slid.bs.carousel.image_gallery\", (ev) => {\n            if (!_previousEditor && !_miniatureClicked) {\n                return;\n            }\n            _previousEditor = undefined;\n            _miniatureClicked = false;\n            // slid.bs.carousel is most of the time fired too soon by bootstrap\n            // since it emulates the transitionEnd with a setTimeout. We wait\n            // here an extra 20% of the time before retargeting edition, which\n            // should be enough...\n            const _slideDuration = new Date().getTime() - lastSlideTimeStamp;\n            setTimeout(() => {\n                const activeImageEl = this.$target[0].querySelector(\".carousel-item.active img\");\n                this.trigger_up(\"activate_snippet\", {\n                    $snippet: $(activeImageEl),\n                    ifInactiveOptions: true,\n                });\n            }, 0.2 * _slideDuration);\n        });\n    },\n});\n\noptions.registry.gallery = options.registry.GalleryLayout.extend({\n    /**\n     * @override\n     */\n    start() {\n        const _super = this._super.bind(this);\n        let layoutPromise;\n        const containerEl = this.$target[0].querySelector(\":scope > .container, :scope > .container-fluid, :scope > .o_container_small\");\n        if (containerEl.querySelector(\":scope > *:not(div)\")) {\n            layoutPromise = this._relayout();\n        } else {\n            layoutPromise = Promise.resolve();\n        }\n        return layoutPromise.then(() => _super.apply(this, arguments).then(() => {\n            // Call specific mode's start if defined (e.g. _slideshowStart)\n            const startMode = this[`_${this._getMode()}Start`];\n            if (startMode) {\n                startMode.bind(this)();\n            }\n        }));\n    },\n    /**\n     * @override\n     */\n    cleanForSave() {\n        if (this.$target.hasClass('slideshow')) {\n            this.$target.removeAttr('style');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Allows to change the number of columns when displaying images with a\n     * grid-like layout.\n     *\n     * @see this.selectClass for parameters\n     */\n    columns(previewMode, widgetValue, params) {\n        const nbColumns = parseInt(widgetValue || '1');\n        this.$target.attr('data-columns', nbColumns);\n\n        return this._relayout();\n    },\n    /**\n     * Allows to change the images layout. @see grid, masonry, nomode, slideshow\n     *\n     * @see this.selectClass for parameters\n     */\n    mode(previewMode, widgetValue, params) {\n        return this._setMode(widgetValue);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'mode': {\n                let activeModeName = 'slideshow';\n                for (const modeName of params.possibleValues) {\n                    if (this.$target.hasClass(`o_${modeName}`)) {\n                        activeModeName = modeName;\n                        break;\n                    }\n                }\n                this.activeMode = activeModeName;\n                return activeModeName;\n            }\n            case 'columns': {\n                return `${this._getColumns()}`;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'slideshow_mode_opt') {\n            return false;\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.GalleryImageList = options.registry.GalleryLayout.extend({\n    /**\n     * @override\n     */\n    start() {\n        // Make sure image previews are updated if images are changed\n        this.$target.on('image_changed.gallery', 'img', ev => {\n            const $img = $(ev.currentTarget);\n            const index = this.$target.find('.carousel-item.active').index();\n            this.$('.carousel:first li[data-bs-target]:eq(' + index + ')')\n                .css('background-image', 'url(' + $img.attr('src') + ')');\n        });\n\n        // When the snippet is empty, an edition button is the default content\n        // TODO find a nicer way to do that to have editor style\n        this.$target.on('click.gallery', '.o_add_images', ev => {\n            ev.stopImmediatePropagation();\n            this.addImages(false);\n        });\n\n        this.$target.on('dropped.gallery', 'img', ev => {\n            this._relayout();\n            if (!ev.target.height) {\n                $(ev.target).one('load', () => {\n                    setTimeout(() => {\n                        this.trigger_up('cover_update');\n                    });\n                });\n            }\n        });\n\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    async onBuilt() {\n        await this._super(...arguments);\n        if (this.$target.find('.o_add_images').length) {\n            await this.addImages(false);\n        }\n        // TODO should consider the async parts\n        this._adaptNavigationIDs();\n    },\n    /**\n     * @override\n     */\n    onClone() {\n        this._adaptNavigationIDs();\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$target.off('.gallery');\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Allows to select images to add as part of the snippet.\n     *\n     * @see this.selectClass for parameters\n     */\n    addImages(previewMode) {\n        const $images = this.$('img');\n        const $container = this.$('> .container, > .container-fluid, > .o_container_small');\n        const lastImage = this._getItemsGallery().at(-1);\n        let index = lastImage ? this._getIndex(lastImage) : -1;\n        return new Promise(resolve => {\n            let savedPromise = Promise.resolve();\n            const props = {\n                multiImages: true,\n                onlyImages: true,\n                save: images => {\n                    const imagePromises = [];\n                    for (const image of images) {\n                        const $img = $('<img/>', {\n                            class: $images.length > 0 ? $images[0].className : 'img img-fluid d-block mh-100 mw-100 mx-auto rounded object-fit-cover',\n                            src: image.src,\n                            'data-index': ++index,\n                            alt: image.alt || '',\n                            'data-name': _t('Image'),\n                            style: $images.length > 0 ? $images[0].style.cssText : '',\n                        }).appendTo($container);\n                        const imgEl = $img[0];\n                        imagePromises.push(new Promise(resolve => {\n                            loadImageInfo(imgEl).then(() => {\n                                if (imgEl.dataset.mimetype && ![\n                                    \"image/gif\",\n                                    \"image/svg+xml\",\n                                    \"image/webp\",\n                                ].includes(imgEl.dataset.mimetype)) {\n                                    // Convert to webp but keep original width.\n                                    imgEl.dataset.mimetype = \"image/webp\";\n                                    applyModifications(imgEl, {\n                                        mimetype: \"image/webp\",\n                                    }).then(src => {\n                                        imgEl.src = src;\n                                        imgEl.classList.add(\"o_modified_image_to_save\");\n                                        resolve();\n                                    });\n                                } else {\n                                    resolve();\n                                }\n                            });\n                        }));\n                    }\n                    savedPromise = Promise.all(imagePromises);\n                    if (images.length > 0) {\n                        savedPromise = savedPromise.then(async () => {\n                            await this._relayout();\n                        });\n                        this.trigger_up('cover_update');\n                    }\n                },\n            };\n            this.call(\"dialog\", \"add\", MediaDialog, props, {\n                onClose: () => {\n                    savedPromise.then(resolve);\n                },\n            });\n        });\n    },\n    /**\n     * Allows to remove all images. Restores the snippet to the way it was when\n     * it was added in the page.\n     *\n     * @see this.selectClass for parameters\n     */\n    removeAllImages(previewMode) {\n        const $addImg = $('<div>', {\n            class: 'alert alert-info css_non_editable_mode_hidden text-center',\n        });\n        const $text = $('<span>', {\n            class: 'o_add_images',\n            style: 'cursor: pointer;',\n            text: _t(\" Add Images\"),\n        });\n        const $icon = $('<i>', {\n            class: ' fa fa-plus-circle',\n        });\n        this._replaceContent($addImg.append($icon).append($text));\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Handles image removals and image index updates.\n     *\n     * @override\n     */\n    notify(name, data) {\n        this._super(...arguments);\n        if (name === 'image_removed') {\n            data.$image.remove(); // Force the removal of the image before reset\n            this.trigger_up('snippet_edition_request', {exec: () => {\n                return this._relayout();\n            }});\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _adaptNavigationIDs() {\n        const uuid = new Date().getTime();\n        this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);\n        this.$target.find('[data-bs-slide], [data-bs-slide-to]').toArray().forEach((el) => {\n            const $el = $(el);\n            if ($el.attr('data-bs-target')) {\n                $el.attr('data-bs-target', '#slideshow_' + uuid);\n            } else if ($el.attr('href')) {\n                $el.attr('href', '#slideshow_' + uuid);\n            }\n        });\n    },\n});\n\noptions.registry.gallery_img = options.Class.extend({\n    /**\n     * Rebuilds the whole gallery when one image is removed.\n     *\n     * @override\n     */\n    onRemove: function () {\n        this.trigger_up('option_update', {\n            optionName: 'GalleryImageList',\n            name: 'image_removed',\n            data: {\n                $image: this.$target,\n            },\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport {_t} from \"@web/core/l10n/translation\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport SocialMediaOption from \"@website/snippets/s_social_media/options\";\n\noptions.registry.InstagramPage = options.Class.extend({\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.notification = this.bindService(\"notification\");\n        this.instagramUrlStr = \"instagram.com/\";\n    },\n    /**\n     * @override\n     */\n    async onBuilt() {\n        // First we check if the user has changed his instagram during the\n        // current edition (via the social media options).\n        const dbSocialValuesCache = SocialMediaOption.getDbSocialValuesCache();\n        let socialInstagram = dbSocialValuesCache && dbSocialValuesCache[\"social_instagram\"];\n        // If not, we check the value in the DB.\n        if (!socialInstagram) {\n            let websiteId;\n            this.trigger_up(\"context_get\", {\n                callback: function (ctx) {\n                    websiteId = ctx[\"website_id\"];\n                },\n            });\n            const values = await this.orm.read(\"website\", [websiteId], [\"social_instagram\"]);\n            socialInstagram = values[0][\"social_instagram\"];\n        }\n        if (socialInstagram) {\n            const pageName = this._getInstagramPageNameFromUrl(socialInstagram);\n            if (pageName) {\n                this.$target[0].dataset.instagramPage = pageName;\n            }\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Registers the instagram page name.\n     *\n     * @see this.selectClass for parameters\n     */\n    async setInstagramPage(previewMode, widgetValue, params) {\n        if (widgetValue.includes(this.instagramUrlStr)) {\n            widgetValue = this._getInstagramPageNameFromUrl(widgetValue);\n        }\n        if (!widgetValue) {\n            this.notification.add(_t(\"The Instagram page name is not valid\"), {\n                type: \"warning\",\n            });\n        }\n        this.$target[0].dataset.instagramPage = widgetValue || \"\";\n        // As the public widget restart is disabled for instagram, we have to\n        // manually restart the widget.\n        await this.trigger_up(\"widgets_start_request\", {\n            $target: this.$target,\n            editableMode: true,\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(widgetName, params) {\n        if (widgetName === \"setInstagramPage\") {\n            return this.$target[0].dataset.instagramPage;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Returns the instagram page name from the given url.\n     *\n     * @private\n     * @param {string} url\n     * @returns {string|undefined}\n     */\n    _getInstagramPageNameFromUrl(url) {\n        const pageName = url.split(this.instagramUrlStr)[1];\n        if (!pageName || pageName.includes(\"?\") || pageName.includes(\"#\") ||\n            (pageName.includes(\"/\") && pageName.split(\"/\")[1].length > 0)) {\n            return;\n        }\n        return pageName.split(\"/\")[0];\n    },\n});\n\nexport default {\n    InstagramPage: options.registry.InstagramPage,\n};\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\n\noptions.registry.CardWidth = options.Class.extend({\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        const value = await this._super(...arguments);\n        if (methodName === \"selectStyle\") {\n            if (params.cssProperty === \"max-width\") {\n                // If no `max-width` is set, consider it to be at 100%.\n                if (!this.$target[0].style[params.cssProperty]) {\n                    return \"100%\";\n                }\n            }\n        } else if (methodName === \"selectClass\" && !value) {\n            // If no alignment has been set, consider it to be set to the left.\n            return \"me-auto\";\n        }\n        return value;\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === \"card_alignment_opt\") {\n            const maxWidth = this.$target[0].style.maxWidth;\n            const isFullWidth = !maxWidth || maxWidth === \"100%\";\n            return !isFullWidth;\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.CardImageOptions = options.Class.extend({\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Injects a new cover image.\n     */\n    addCoverImage() {\n        const imageWrapperEl = renderToElement(\"website.s_card.imageWrapper\");\n        this.$target[0].insertAdjacentElement(\"afterbegin\", imageWrapperEl);\n        this.$target[0].classList.add(\"o_card_img_top\");\n    },\n    /**\n     * Changes the cover image position.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectImageClass(previewMode, widgetValue, params) {\n        const imageEl = this.$target[0].querySelector(\".o_card_img\");\n        for (const className of params.possibleValues) {\n            if (className) {\n                imageEl.classList.remove(className);\n            }\n        }\n        imageEl.classList.add(widgetValue);\n\n        // Removing invalid ratio classes when changing the image position.\n        const imageWrapperEl = this.$target[0].querySelector(\".o_card_img_wrapper\");\n        if (previewMode === true) {\n            // If the image has a non-square ratio, force the ratio to be square\n            // when setting the image as horizontal, as only the \"Square\" ratio\n            // is available in that case (so the \"Ratio\" widget is consistent).\n            if ([\"rounded-start\", \"rounded-end\"].includes(widgetValue)\n                    && this.$target[0].querySelector(\".ratio:not(.ratio-1x1)\")) {\n                const ratioClassRegex = /(ratio-4x3|ratio-16x9|ratio-21x9|o_card_img_ratio_custom)/g;\n                const ratioClass = imageWrapperEl.className.match(ratioClassRegex);\n                if (ratioClass) {\n                    this.previousRatio = ratioClass[0];\n                    imageWrapperEl.classList.remove(this.previousRatio);\n                    imageWrapperEl.classList.add(\"ratio-1x1\");\n                }\n            }\n        } else if (previewMode === false) {\n            delete this.previousRatio;\n        } else {\n            if (this.previousRatio) {\n                imageWrapperEl.classList.remove(\"ratio-1x1\");\n                imageWrapperEl.classList.add(this.previousRatio);\n                delete this.previousRatio;\n            }\n        }\n    },\n    /**\n     * Removes the cover image.\n     */\n    removeCoverImage() {\n        const imageWrapperEl = this.$target[0].querySelector(\".o_card_img_wrapper\");\n        imageWrapperEl.remove();\n\n        // Remove the classes and styles linked to the wrapper .\n        const imageWrapperClasses = [\"o_card_img_top\", \"o_card_img_horizontal\", \"flex-lg-row\", \"flex-lg-row-reverse\"];\n        this.$target[0].classList.remove(...imageWrapperClasses);\n        this.$target[0].style.removeProperty(\"--card-img-size-h\");\n        this.$target[0].style.removeProperty(\"--card-img-ratio-align\");\n        this.$target[0].style.removeProperty(\"--card-img-aspect-ratio\");\n    },\n    /**\n     * Aligns the image inside the cover.\n     *\n     * @private\n     */\n    alignCoverImage() {\n        const ratio = this._getImageToWrapperRatio();\n        const imageWrapperEl = this.$target[0].querySelector(\".o_card_img_wrapper\");\n\n        imageWrapperEl.classList.toggle(\"o_card_img_adjust_v\", ratio > 1);\n        imageWrapperEl.classList.toggle(\"o_card_img_adjust_h\", ratio < 1);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        const hasCoverImage = !!this.$target[0].querySelector(\".o_card_img_wrapper\");\n        const useRatio = !!this.$target[0].querySelector(\".o_card_img_wrapper.ratio\");\n        const hasNonSquareRatio = this._getImageToWrapperRatio() !== 1;\n        const hasShape = !!this.$target[0].querySelector(\".o_card_img[data-shape]\");\n\n        if (widgetName === \"add_cover_image_opt\") {\n            return !hasCoverImage;\n        } else if ([\"cover_image_position_opt\", \"remove_cover_image_opt\", \"cover_image_width_opt\",\n                \"cover_image_ratio_range_opt\"].includes(widgetName)) {\n            return hasCoverImage;\n        } else if (widgetName === \"cover_image_alignment_opt\") {\n            return hasCoverImage && hasNonSquareRatio && useRatio && !hasShape;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Compares the aspect ratio of the card image to its wrapper.\n     *\n     * @private\n     * @returns {number} Ratio comparison value:\n     *                   -  1: img and wrapper have identical aspect ratios\n     *                   - <1: img is more portrait (taller) than wrapper\n     *                   - >1: img is more landscape (wider) than wrapper\n     */\n    _getImageToWrapperRatio() {\n        const imageEl = this.$target[0].querySelector(\".o_card_img\");\n        const imageWrapperEl = this.$target[0].querySelector(\".o_card_img_wrapper\");\n        if (!imageEl || !imageWrapperEl) {\n            return false;\n        }\n\n        const imgRatio = imageEl.naturalHeight / imageEl.naturalWidth;\n        const wrapperRatio = imageWrapperEl.offsetHeight / imageWrapperEl.offsetWidth;\n\n        return imgRatio / wrapperRatio;\n    },\n});\n", "import options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.faqHorizontalMultipleItems = options.registry.MultipleItems.extend({\n    _addItemCallback() {\n        // Find the iframe and its #wrapwrap\n        const iframe = document.querySelector('.o_iframe');\n        const iframeDocument = iframe.contentDocument || iframe.contentWindow.document;\n        const wrapwrap = iframeDocument.getElementById('wrapwrap');\n\n        const topics = this.$target[0].getElementsByClassName('s_faq_horizontal_entry');\n        const newTopic = topics[topics.length - 1];\n        const newTopicRect = newTopic.getBoundingClientRect();\n        const wrapwrapRect = wrapwrap.getBoundingClientRect();\n\n        const scrollTop = wrapwrap.scrollTop;\n        const centerY = (newTopicRect.top - wrapwrapRect.top) + scrollTop - (wrapwrap.clientHeight / 2) + (newTopicRect.height / 2);\n\n        wrapwrap.scrollTo({\n            top: centerY,\n            behavior: 'smooth'\n        });\n    }\n});\n", "import options from \"@web_editor/js/editor/snippets.options\";\nimport \"@website/js/editor/snippets.options\";\n\noptions.registry.CarouselIntro = options.registry.Carousel.extend({\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        // Prevent the \"Controllers\" option from being \"centered\" when\n        // arrows and indicators are displayed.\n        if (methodName === \"selectClass\" && params.name === \"carousel_controllers_centered_opt\") {\n            const controllersEl = this.$target[0];\n            const carouselEl = controllersEl.closest(\".carousel\");\n            const indicatorsEl = controllersEl.querySelector(\".carousel-indicators\");\n            if (\n                !carouselEl.classList.contains(\"s_carousel_arrows_hidden\")\n                && !indicatorsEl.classList.contains(\"s_carousel_indicators_hidden\")\n            )\n            {\n                controllersEl.classList.toggle(\"justify-content-center\");\n                controllersEl.classList.toggle(\"justify-content-between\");\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.countdown = options.Class.extend({\n    events: Object.assign({}, options.Class.prototype.events || {}, {\n        'click .toggle-edit-message': '_onToggleEndMessageClick',\n    }),\n\n    /**\n     * Remove any preview classes, if present.\n     *\n     * @override\n     */\n    cleanForSave: async function () {\n        this.$target.find('.s_countdown_canvas_wrapper').removeClass(\"s_countdown_none\");\n        this.$target.find('.s_countdown_end_message').removeClass(\"s_countdown_enable_preview\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Changes the countdown action at zero.\n     *\n     * @see this.selectClass for parameters\n     */\n    endAction: function (previewMode, widgetValue, params) {\n        this.$target[0].dataset.endAction = widgetValue;\n        if (widgetValue === 'message' || widgetValue === 'message_no_countdown') {\n            if (!this.$target.find('.s_countdown_end_message').length) {\n                const message = this.endMessage || renderToElement('website.s_countdown.end_message');\n                this.$target.append(message);\n            }\n            this.$target.toggleClass('hide-countdown', widgetValue === 'message_no_countdown');\n        } else {\n            const $message = this.$target.find('.s_countdown_end_message').detach();\n            if (this.showEndMessage) {\n                this._onToggleEndMessageClick();\n            }\n            if ($message.length) {\n                this.endMessage = $message[0].outerHTML;\n            }\n        }\n    },\n    /**\n    * Changes the countdown style.\n    *\n    * @see this.selectClass for parameters\n    */\n    layout: function (previewMode, widgetValue, params) {\n        switch (widgetValue) {\n            case 'circle':\n                this.$target[0].dataset.progressBarStyle = 'disappear';\n                this.$target[0].dataset.progressBarWeight = 'thin';\n                this.$target[0].dataset.layoutBackground = 'none';\n                break;\n            case 'boxes':\n                this.$target[0].dataset.progressBarStyle = 'none';\n                this.$target[0].dataset.layoutBackground = 'plain';\n                break;\n            case 'clean':\n                this.$target[0].dataset.progressBarStyle = 'none';\n                this.$target[0].dataset.layoutBackground = 'none';\n                break;\n            case 'text':\n                this.$target[0].dataset.progressBarStyle = 'none';\n                this.$target[0].dataset.layoutBackground = 'none';\n                break;\n        }\n        this.$target[0].dataset.layout = widgetValue;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    updateUIVisibility: async function () {\n        await this._super(...arguments);\n        const dataset = this.$target[0].dataset;\n\n        // End Action UI\n        this.$el.find('.toggle-edit-message')\n            .toggleClass('d-none', dataset.endAction === 'nothing' || dataset.endAction === 'redirect');\n\n        // End Message UI\n        this.updateUIEndMessage();\n    },\n    /**\n     * @see this.updateUI\n     */\n    updateUIEndMessage: function () {\n        this.$target.find('.s_countdown_canvas_wrapper')\n            .toggleClass(\"s_countdown_none\", this.showEndMessage === true && this.$target.hasClass(\"hide-countdown\"));\n        this.$target.find('.s_countdown_end_message')\n            .toggleClass(\"s_countdown_enable_preview\", this.showEndMessage === true);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'endAction':\n            case 'layout':\n                return this.$target[0].dataset[methodName];\n\n            case 'selectDataAttribute': {\n                if (params.colorNames) {\n                    params.attributeDefaultValue = 'rgba(0, 0, 0, 255)';\n                }\n                break;\n            }\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onToggleEndMessageClick: function () {\n        this.showEndMessage = !this.showEndMessage;\n        this.$el.find(\".toggle-edit-message\")\n            .toggleClass('text-primary', this.showEndMessage);\n        this.updateUIEndMessage();\n        this.trigger_up('cover_update');\n    },\n});\n", "/** @odoo-module */\n\nimport options from '@web_editor/js/editor/snippets.options';\n\noptions.registry.MasonryLayout = options.registry.SelectTemplate.extend({\n    /**\n     * @constructor\n     */\n    init() {\n        this._super(...arguments);\n        this.containerSelector = '> .container, > .container-fluid, > .o_container_small';\n        this.selectTemplateWidgetName = 'masonry_template_opt';\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    selectTemplate(previewMode, widgetValue, params) {\n        // TODO remove in master, needed to fix broken masonry block in\n        // avantgarde theme in outdated databases.\n        if (!this.$target.find(this.containerSelector).length) {\n            this.containerEl = this.$target[0].ownerDocument.createElement('div');\n            this.containerEl.classList.add('container-fluid');\n            this.$target[0].appendChild(this.containerEl);\n            this.containerEl.appendChild(this.$target[0].querySelector(':scope > .row'));\n        }\n        return this._super(...arguments);\n    },\n\n    /**\n     * Changes the container class according to the template.\n     *\n     * @see this.selectClass for parameters\n     */\n    selectContainerClass(previewMode, widgetValue, params) {\n        const containerEl = this.$target[0].firstElementChild;\n        const containerClasses = [\"container\", \"container-fluid\", \"o_container_small\"];\n        if (!containerClasses.some(cls => containerEl.classList.contains(cls))) {\n            return;\n        }\n        containerEl.classList.remove(...containerClasses);\n        containerEl.classList.add(widgetValue);\n    },\n});\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.SnippetPopup = options.Class.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        // Note: the link are excluded here so that internal modal buttons do\n        // not close the popup as we want to allow edition of those buttons.\n        this.$bsTarget.on('click.SnippetPopup', '.js_close_popup:not(a, .btn)', ev => {\n            ev.stopPropagation();\n            this.onTargetHide();\n            this.trigger_up('snippet_option_visibility_update', {show: false});\n        });\n        this.$bsTarget.on('shown.bs.modal.SnippetPopup', () => {\n            this.trigger_up('snippet_option_visibility_update', {show: true});\n            // TODO duplicated code from the popup public widget, this should\n            // be moved to a *video* public widget and be reviewed in master\n            this.$target[0].querySelectorAll('.media_iframe_video').forEach(media => {\n                const iframe = media.querySelector('iframe');\n                iframe.src = media.dataset.oeExpression || media.dataset.src; // TODO still oeExpression to remove someday\n            });\n        });\n        this.$bsTarget.on('hide.bs.modal.SnippetPopup', () => {\n            this.trigger_up('snippet_option_visibility_update', {show: false});\n            this._removeIframeSrc();\n        });\n        // The video might be playing before entering edit mode (possibly with\n        // sound). Stop the video, as the user can't do it (no button on video\n        // in edit mode).\n        this._removeIframeSrc();\n        if (!this.$target[0].parentElement.matches(\"#website_cookies_bar\")) {\n            this.trigger_up(\"option_update\", {\n                optionName: \"anchor\",\n                name: \"modalAnchor\",\n                data: {\n                    buttonEl: this._requestUserValueWidgets(\"onclick_opt\")[0].el,\n                },\n            });\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this._super(...arguments);\n        // The video should not start before the modal opens, remove it from the\n        // DOM. It will be added back on modal open to start the video.\n        this._removeIframeSrc();\n        this.$bsTarget.off('.SnippetPopup');\n    },\n    /**\n     * @override\n     */\n    onBuilt: function () {\n        this._assignUniqueID();\n        // Fix in stable to convert the data-focus bootstrap option from version 4.0 to\n        // 5.1 (renamed to data-bs-focus).\n        const popup = this.$target.closest('.s_popup_middle');\n        if (popup && popup.attr('data-focus')) {\n            popup.attr('data-bs-focus', popup.attr('data-focus'));\n            popup[0].removeAttribute('data-focus');\n        }\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this._assignUniqueID();\n    },\n    /**\n     * @override\n     */\n    onTargetShow: async function () {\n        this.$bsTarget.modal('show');\n        $(this.$target[0].ownerDocument.body).children('.modal-backdrop:last').addClass('d-none');\n    },\n    /**\n     * @override\n     */\n    onTargetHide: async function () {\n        return new Promise(resolve => {\n            const timeoutID = setTimeout(() => {\n                this.$bsTarget.off('hidden.bs.modal.popup_on_target_hide');\n                resolve();\n            }, 500);\n            this.$bsTarget.one('hidden.bs.modal.popup_on_target_hide', () => {\n                clearTimeout(timeoutID);\n                resolve();\n            });\n            // The following line is in charge of hiding .s_popup at the same\n            // time the modal is closed when the page is saved in edit mode.\n            this.$target[0].closest('.s_popup').classList.add('d-none');\n            this.$bsTarget.modal('hide');\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Moves the snippet in #o_shared_blocks to be common to all pages or inside\n     * the first editable oe_structure in the main to be on current page only.\n     *\n     * @see this.selectClass for parameters\n     */\n    moveBlock: function (previewMode, widgetValue, params) {\n        const selector = widgetValue === 'allPages' ?\n            '#o_shared_blocks' : 'main .oe_structure:o_editable';\n        const whereEl = $(this.$target[0].ownerDocument).find(selector)[0];\n        const popupEl = this.$target[0].closest('.s_popup');\n        whereEl.prepend(popupEl);\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setBackdrop(previewMode, widgetValue, params) {\n        const color = widgetValue ? 'var(--black-50)' : '';\n        this.$target[0].style.setProperty('background-color', color, 'important');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Creates a unique ID.\n     *\n     * @private\n     */\n    _assignUniqueID: function () {\n        this.$target.closest('.s_popup').attr('id', 'sPopup' + Date.now());\n    },\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'moveBlock':\n                return this.$target[0].closest('#o_shared_blocks') ? 'allPages' : 'currentPage';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Removes the iframe `src` attribute (a copy of the src is already on the\n     * parent `oe-expression` attribute).\n     *\n     * @private\n     */\n    _removeIframeSrc() {\n        this.$target.find('.media_iframe_video iframe').each((i, iframe) => {\n            iframe.src = '';\n        });\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport { isCSSColor } from '@web/core/utils/colors';\n\noptions.registry.InnerChart = options.Class.extend({\n    custom_events: Object.assign({}, options.Class.prototype.custom_events, {\n        'get_custom_colors': '_onGetCustomColors',\n    }),\n    events: Object.assign({}, options.Class.prototype.events, {\n        'click we-button.add_column': '_onAddColumnClick',\n        'click we-button.add_row': '_onAddRowClick',\n        'click we-button.o_we_matrix_remove_col': '_onRemoveColumnClick',\n        'click we-button.o_we_matrix_remove_row': '_onRemoveRowClick',\n        'input we-matrix input': '_onMatrixInputInput',\n        'focus we-matrix input': '_onMatrixInputFocus',\n    }),\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this.themeArray = ['o-color-1', 'o-color-2', 'o-color-3', 'o-color-4', 'o-color-5'];\n        this.style = window.getComputedStyle(this.$target[0].ownerDocument.documentElement);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.backSelectEl = this.el.querySelector('[data-name=\"chart_bg_color_opt\"]');\n        this.borderSelectEl = this.el.querySelector('[data-name=\"chart_border_color_opt\"]');\n\n        // Build matrix content\n        this.tableEl = this.el.querySelector('we-matrix table');\n        const data = JSON.parse(this.$target[0].dataset.data);\n        data.labels.forEach(el => {\n            this._addRow(el);\n        });\n        data.datasets.forEach((el, i) => {\n            if (this._isPieChart()) {\n                // Add header colors in case the user changes the type of graph\n                const headerBackgroundColor = this.themeArray[i] || this._randomColor();\n                const headerBorderColor = this.themeArray[i] || this._randomColor();\n                this._addColumn(el.label, el.data, headerBackgroundColor, headerBorderColor, el.backgroundColor, el.borderColor);\n            } else {\n                this._addColumn(el.label, el.data, el.backgroundColor, el.borderColor);\n            }\n        });\n        this._displayRemoveColButton();\n        this._displayRemoveRowButton();\n        this._setDefaultSelectedInput();\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    updateUI: async function () {\n        // Selected input might not be in dom anymore if col/row removed\n        // Done before _super because _computeWidgetState of colorChange\n        if (!this.lastEditableSelectedInput.closest('table') || this.colorPaletteSelectedInput && !this.colorPaletteSelectedInput.closest('table')) {\n            this._setDefaultSelectedInput();\n        }\n\n        await this._super(...arguments);\n\n        this.backSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t(\"Data Color\") : _t(\"Dataset Color\");\n        this.borderSelectEl.querySelector('we-title').textContent = this._isPieChart() ? _t(\"Data Border\") : _t(\"Dataset Border\");\n\n        // Dataset/Cell color\n        this.tableEl.querySelectorAll('input').forEach(el => el.style.border = '');\n        const selector = this._isPieChart() ? 'td input' : 'tr:first-child input';\n        this.tableEl.querySelectorAll(selector).forEach(el => {\n            const color = el.dataset.backgroundColor || el.dataset.borderColor;\n            if (color) {\n                el.style.border = '2px solid';\n                el.style.borderColor = isCSSColor(color) ? color : weUtils.getCSSVariableValue(color, this.style);\n            }\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Set the color on the selected input.\n     */\n    colorChange: async function (previewMode, widgetValue, params) {\n        if (widgetValue) {\n            this.colorPaletteSelectedInput.dataset[params.attributeName] = widgetValue;\n        } else {\n            delete this.colorPaletteSelectedInput.dataset[params.attributeName];\n        }\n        await this._reloadGraph();\n        // To focus back the input that is edited we have to wait for the color\n        // picker to be fully reloaded.\n        await new Promise(resolve => setTimeout(() => {\n            this.lastEditableSelectedInput.focus();\n            resolve();\n        }));\n    },\n    /**\n     * @override\n     */\n    selectDataAttribute: async function (previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        // Data might change if going from or to a pieChart.\n        if (params.attributeName === 'type') {\n            this._setDefaultSelectedInput();\n            await this._reloadGraph();\n        }\n        if (params.attributeName === 'minValue' || params.attributeName === 'maxValue') {\n            this._computeTicksMinMaxValue();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'colorChange') {\n            return this.colorPaletteSelectedInput && this.colorPaletteSelectedInput.dataset[params.attributeName] || '';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility: function (widgetName, params) {\n        switch (widgetName) {\n            case 'stacked_chart_opt': {\n                return this._getColumnCount() > 1;\n            }\n            case 'chart_bg_color_opt':\n            case 'chart_border_color_opt': {\n                return !!this.colorPaletteSelectedInput;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Maintains the gap between the scale axis for the auto fit behavior if we\n     * used min/max config.\n     *\n     * @private\n     */\n    _computeTicksMinMaxValue() {\n        const dataset = this.$target[0].dataset;\n        let minValue = parseInt(dataset.minValue);\n        let maxValue = parseInt(dataset.maxValue);\n        if (!isNaN(maxValue)) {\n            // Reverse min max values when min value is greater than max value\n            if (maxValue < minValue) {\n                maxValue = minValue;\n                minValue = parseInt(dataset.maxValue);\n            } else if (maxValue === minValue) {\n                // If min value and max value are same for positive and negative\n                // number\n                minValue < 0 ? (maxValue = 0, minValue = 2 * minValue) : (minValue = 0, maxValue = 2 * maxValue);\n            }\n        } else {\n            // Find max value from each row/column data\n            const datasets = JSON.parse(dataset.data).datasets || [];\n            const dataValue = datasets\n                .map((el) => {\n                    return el.data.map((data) => {\n                        return !isNaN(parseInt(data)) ? parseInt(data) : 0;\n                    });\n                })\n                .flat();\n            // When max value is not given and min value is greater than chart\n            // data values\n            if (minValue >= Math.max(...dataValue)) {\n                maxValue = minValue;\n                minValue = 0;\n            }\n        }\n        this.$target.attr({\n            'data-ticks-min': minValue,\n            'data-ticks-max': maxValue,\n        });\n    },\n    /**\n     * Sets and reloads the data on the canvas if it has changed.\n     * Used in matrix related method.\n     *\n     * @private\n     */\n    _reloadGraph: async function () {\n        const jsonValue = this._matrixToChartData();\n        if (this.$target[0].dataset.data !== jsonValue) {\n            this.$target[0].dataset.data = jsonValue;\n            await this._refreshPublicWidgets();\n        }\n    },\n    /**\n     * Return a stringifyed chart.js data object from the matrix\n     * Pie charts have one color per data while other charts have one color per dataset.\n     *\n     * @private\n     */\n    _matrixToChartData: function () {\n        const data = {\n            labels: [],\n            datasets: [],\n        };\n        this.tableEl.querySelectorAll('tr:first-child input').forEach(el => {\n            data.datasets.push({\n                label: el.value || '',\n                data: [],\n                backgroundColor: this._isPieChart() ? [] : el.dataset.backgroundColor || '',\n                borderColor: this._isPieChart() ? [] : el.dataset.borderColor || '',\n            });\n        });\n        this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el) => {\n            const title = el.querySelector('th input').value || '';\n            data.labels.push(title);\n            el.querySelectorAll('td input').forEach((el, i) => {\n                data.datasets[i].data.push(el.value || 0);\n                if (this._isPieChart()) {\n                    data.datasets[i].backgroundColor.push(el.dataset.backgroundColor || '');\n                    data.datasets[i].borderColor.push(el.dataset.borderColor || '');\n                }\n            });\n        });\n        return JSON.stringify(data);\n    },\n    /**\n     * Return a td containing a we-button with minus icon\n     *\n     * @param  {...string} classes Classes to add to the we-button\n     * @returns {HTMLElement}\n     */\n    _makeDeleteButton: function (...classes) {\n        const rmbuttonEl = options.buildElement('we-button', null, {\n            classes: ['o_we_text_danger', 'o_we_link', 'fa', 'fa-fw', 'fa-minus', ...classes],\n        });\n        rmbuttonEl.title = classes.includes('o_we_matrix_remove_col') ? _t(\"Remove Serie\") : _t(\"Remove Row\");\n        const newEl = document.createElement('td');\n        newEl.appendChild(rmbuttonEl);\n        return newEl;\n    },\n    /**\n     * Add a column to the matrix\n     * The th (dataset label) of a column hold the colors for the entire dataset if the graph is not a pie chart\n     * If the graph is a pie chart the color of the td (data) are used.\n     *\n     * @private\n     * @param {String} title The title of the column\n     * @param {Array} values The values of the column input\n     * @param {String} heardeBackgroundColor The background color of the dataset\n     * @param {String} headerBorderColor The border color of the dataset\n     * @param {string[]} cellBackgroundColors The background colors of the datas inputs, random color if missing\n     * @param {string[]} cellBorderColors The border color of the datas inputs, no color if missing\n     */\n    _addColumn: function (title, values, heardeBackgroundColor, headerBorderColor, cellBackgroundColors = [], cellBorderColors = []) {\n        const firstRow = this.tableEl.querySelector('tr:first-child');\n        const headerInput = this._makeCell('th', title, heardeBackgroundColor, headerBorderColor);\n        firstRow.insertBefore(headerInput, firstRow.lastElementChild);\n\n        this.tableEl.querySelectorAll('tr:not(:first-child):not(:last-child)').forEach((el, i) => {\n            const newCell = this._makeCell('td', values ? values[i] : null, cellBackgroundColors[i] || this._randomColor(), cellBorderColors[i - 1]);\n            el.insertBefore(newCell, el.lastElementChild);\n        });\n\n        const lastRow = this.tableEl.querySelector('tr:last-child');\n        const removeButton = this._makeDeleteButton('o_we_matrix_remove_col');\n        lastRow.appendChild(removeButton);\n    },\n    /**\n     * Add a row to the matrix\n     * The background color of the datas are random\n     *\n     * @private\n     * @param {String} tilte The title of the row\n     */\n    _addRow: function (tilte) {\n        const trEl = document.createElement('tr');\n        trEl.appendChild(this._makeCell('th', tilte));\n        this.tableEl.querySelectorAll('tr:first-child input').forEach(() => {\n            trEl.appendChild(this._makeCell('td', null, this._randomColor()));\n        });\n        trEl.appendChild(this._makeDeleteButton('o_we_matrix_remove_row'));\n        const tbody = this.tableEl.querySelector('tbody');\n        tbody.insertBefore(trEl, tbody.lastElementChild);\n    },\n    /**\n     * @private\n     * @param {string} tag tag of the HTML Element (td/th)\n     * @param {string} value The current value of the cell input\n     * @param {string} backgroundColor The background Color of the data on the graph\n     * @param {string} borderColor The border Color of the data on the graph\n     * @returns {HTMLElement}\n     */\n    _makeCell: function (tag, value, backgroundColor, borderColor) {\n        const newEl = document.createElement(tag);\n        const contentEl = document.createElement('input');\n        contentEl.type = 'text';\n        if (tag === 'td') {\n            contentEl.type = 'number';\n        }\n        contentEl.value = value || '';\n        if (backgroundColor) {\n            contentEl.dataset.backgroundColor = backgroundColor;\n        }\n        if (borderColor) {\n            contentEl.dataset.borderColor = borderColor;\n        }\n        newEl.appendChild(contentEl);\n        return newEl;\n    },\n    /**\n     * Display the remove button coresponding to the colIndex\n     *\n     * @private\n     * @param {Int} colIndex Can be undefined, if so the last remove button of the column will be shown\n     */\n    _displayRemoveColButton: function (colIndex) {\n        if (this._getColumnCount() > 1) {\n            this._displayRemoveButton(colIndex, 'o_we_matrix_remove_col');\n        }\n    },\n    /**\n     * Display the remove button coresponding to the rowIndex\n     *\n     * @private\n     * @param {Int} rowIndex Can be undefined, if so the last remove button of the row will be shown\n     */\n    _displayRemoveRowButton: function (rowIndex) {\n        //Nbr of row minus header and button\n        const rowCount = this.tableEl.rows.length - 2;\n        if (rowCount > 1) {\n            this._displayRemoveButton(rowIndex, 'o_we_matrix_remove_row');\n        }\n    },\n    /**\n     * @private\n     * @param {Int} tdIndex Can be undefined, if so the last remove button will be shown\n     * @param {String} btnClass Either o_we_matrix_remove_col or o_we_matrix_remove_row\n     */\n    _displayRemoveButton: function (tdIndex, btnClass) {\n        const removeBtn = this.tableEl.querySelectorAll(`td we-button.${btnClass}`);\n        removeBtn.forEach(el => el.style.display = ''); //hide all\n        const index = tdIndex < removeBtn.length ? tdIndex : removeBtn.length - 1;\n        removeBtn[index].style.display = 'inline-block';\n    },\n    /**\n     * @private\n     * @return {boolean}\n     */\n    _isPieChart: function () {\n        return ['pie', 'doughnut'].includes(this.$target[0].dataset.type);\n    },\n    /**\n     * Return the number of column minus header and button\n     * @private\n     * @return {integer}\n     */\n    _getColumnCount: function () {\n        return this.tableEl.rows[0].cells.length - 2;\n    },\n    /**\n     * Select the first data input\n     *\n     * @private\n     */\n    _setDefaultSelectedInput: function () {\n        this.lastEditableSelectedInput = this.tableEl.querySelector('td input');\n        if (this._isPieChart()) {\n            this.colorPaletteSelectedInput = this.lastEditableSelectedInput;\n        } else {\n            this.colorPaletteSelectedInput = this.tableEl.querySelector('th input');\n        }\n    },\n    /**\n     * Return a random hexadecimal color.\n     *\n     * @private\n     * @return {string}\n     */\n    _randomColor: function () {\n        return '#' + ('00000' + (Math.random() * (1 << 24) | 0).toString(16)).slice(-6).toUpperCase();\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Used by colorPalette to retrieve the custom colors used on the chart\n     * Make an array with all the custom colors used on the chart\n     * and apply it to the onSuccess method provided by the trigger_up.\n     *\n     * @private\n     */\n    _onGetCustomColors: function (ev) {\n        const data = JSON.parse(this.$target[0].dataset.data || '');\n        let customColors = [];\n        data.datasets.forEach(el => {\n            if (this._isPieChart()) {\n                customColors = customColors.concat(el.backgroundColor).concat(el.borderColor);\n            } else {\n                customColors.push(el.backgroundColor);\n                customColors.push(el.borderColor);\n            }\n        });\n        customColors = customColors.filter((el, i, array) => {\n            return !weUtils.getCSSVariableValue(el, this.style) && array.indexOf(el) === i && el !== ''; // unique non class not transparent\n        });\n        ev.data.onSuccess(customColors);\n    },\n    /**\n     * Add a row at the end of the matrix and display it's remove button\n     * Choose the color of the column from the theme array or a random color if they are already used\n     *\n     * @private\n     */\n    _onAddColumnClick: function () {\n        const usedColor = Array.from(this.tableEl.querySelectorAll('tr:first-child input')).map(el => el.dataset.backgroundColor);\n        const color = this.themeArray.filter(el => !usedColor.includes(el))[0] || this._randomColor();\n        this._addColumn(null, null, color, color);\n        this._reloadGraph().then(() => {\n            this._displayRemoveColButton();\n            this.updateUI();\n        });\n    },\n    /**\n     * Add a column at the end of the matrix and display it's remove button\n     *\n     * @private\n     */\n    _onAddRowClick: function () {\n        this._addRow();\n        this._reloadGraph().then(() => {\n            this._displayRemoveRowButton();\n            this.updateUI();\n        });\n    },\n    /**\n     * Remove the column and show the remove button of the next column or the last if no next.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onRemoveColumnClick: function (ev) {\n        const cell = ev.currentTarget.parentElement;\n        const cellIndex = cell.cellIndex;\n        this.tableEl.querySelectorAll('tr').forEach((el) => {\n            el.children[cellIndex].remove();\n        });\n        this._displayRemoveColButton(cellIndex - 1);\n        this._reloadGraph().then(() => {\n            this.updateUI();\n        });\n    },\n    /**\n     * Remove the row and show the remove button of the next row or the last if no next.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onRemoveRowClick: function (ev) {\n        const row = ev.currentTarget.parentElement.parentElement;\n        const rowIndex = row.rowIndex;\n        row.remove();\n        this._displayRemoveRowButton(rowIndex - 1);\n        this._reloadGraph().then(() => {\n            this.updateUI();\n        });\n    },\n    /**\n     * @private\n     */\n    _onMatrixInputInput() {\n        this._reloadGraph();\n    },\n    /**\n     * Set the selected cell/header and display the related remove button\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onMatrixInputFocus: function (ev) {\n        this.lastEditableSelectedInput = ev.target;\n        const col = ev.target.parentElement.cellIndex;\n        const row = ev.target.parentElement.parentElement.rowIndex;\n        if (this._isPieChart()) {\n            this.colorPaletteSelectedInput = ev.target.parentNode.tagName === 'TD' ? ev.target : null;\n        } else {\n            this.colorPaletteSelectedInput = this.tableEl.querySelector(`tr:first-child th:nth-of-type(${col + 1}) input`);\n        }\n        if (col > 0) {\n            this._displayRemoveColButton(col - 1);\n        }\n        if (row > 0) {\n            this._displayRemoveRowButton(row - 1);\n        }\n        this.updateUI();\n    },\n});\n", "/** @odoo-module **/\n\nimport { MediaDialog } from \"@web_editor/components/media_dialog/media_dialog\";\n\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.Rating = options.Class.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this.iconType = this.$target[0].dataset.icon;\n        this.faClassActiveCustomIcons = this.$target[0].dataset.activeCustomIcon || '';\n        this.faClassInactiveCustomIcons = this.$target[0].dataset.inactiveCustomIcon || '';\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Displays the selected icon type.\n     *\n     * @see this.selectClass for parameters\n     */\n    setIcons: function (previewMode, widgetValue, params) {\n        this.iconType = widgetValue;\n        this._renderIcons();\n        this.$target[0].dataset.icon = widgetValue;\n        delete this.$target[0].dataset.activeCustomIcon;\n        delete this.$target[0].dataset.inactiveCustomIcon;\n    },\n    /**\n     * Allows to select a font awesome icon with media dialog.\n     *\n     * @see this.selectClass for parameters\n     */\n    customIcon: async function (previewMode, widgetValue, params) {\n        const media = document.createElement('i');\n        media.className = params.customActiveIcon === 'true' ? this.faClassActiveCustomIcons : this.faClassInactiveCustomIcons;\n        this.call(\"dialog\", \"add\", MediaDialog, {\n            noImages: true,\n            noDocuments: true,\n            noVideos: true,\n            media,\n            save: icon => {\n                const customClass = icon.className;\n                const $activeIcons = this.$target.find('.s_rating_active_icons > i');\n                const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i');\n                const $icons = params.customActiveIcon === 'true' ? $activeIcons : $inactiveIcons;\n                $icons.removeClass().addClass(customClass);\n                this.faClassActiveCustomIcons = $activeIcons.length > 0 ? $activeIcons.attr('class') : customClass;\n                this.faClassInactiveCustomIcons = $inactiveIcons.length > 0 ? $inactiveIcons.attr('class') : customClass;\n                this.$target[0].dataset.activeCustomIcon = this.faClassActiveCustomIcons;\n                this.$target[0].dataset.inactiveCustomIcon = this.faClassInactiveCustomIcons;\n                this.$target[0].dataset.icon = 'custom';\n                this.iconType = 'custom';\n            }\n        });\n    },\n    /**\n     * Sets the number of active icons.\n     *\n     * @see this.selectClass for parameters\n     */\n    activeIconsNumber: function (previewMode, widgetValue, params) {\n        this.nbActiveIcons = parseInt(widgetValue);\n        this._createIcons();\n    },\n    /**\n     * Sets the total number of icons.\n     *\n     * @see this.selectClass for parameters\n     */\n    totalIconsNumber: function (previewMode, widgetValue, params) {\n        this.nbTotalIcons = Math.max(parseInt(widgetValue), 1);\n        this._createIcons();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'setIcons': {\n                return this.$target[0].dataset.icon;\n            }\n            case 'activeIconsNumber': {\n                this.nbActiveIcons = this.$target.find('.s_rating_active_icons > i').length;\n                return this.nbActiveIcons;\n            }\n            case 'totalIconsNumber': {\n                this.nbTotalIcons = this.$target.find('.s_rating_icons i').length;\n                return this.nbTotalIcons;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Creates the icons.\n     *\n     * @private\n     */\n    _createIcons: function () {\n        const $activeIcons = this.$target.find('.s_rating_active_icons');\n        const $inactiveIcons = this.$target.find('.s_rating_inactive_icons');\n        this.$target.find('.s_rating_icons i').remove();\n        for (let i = 0; i < this.nbTotalIcons; i++) {\n            if (i < this.nbActiveIcons) {\n                $activeIcons.append('<i></i> ');\n            } else {\n                $inactiveIcons.append('<i></i> ');\n            }\n        }\n        this._renderIcons();\n    },\n    /**\n     * Renders icons with selected fonts.\n     *\n     * @private\n     */\n    _renderIcons: function () {\n        const icons = {\n            'fa-star': 'fa-star-o',\n            'fa-thumbs-up': 'fa-thumbs-o-up',\n            'fa-circle': 'fa-circle-o',\n            'fa-square': 'fa-square-o',\n            'fa-heart': 'fa-heart-o'\n        };\n        const faClassActiveIcons = (this.iconType === \"custom\") ? this.faClassActiveCustomIcons : 'fa ' + this.iconType;\n        const faClassInactiveIcons = (this.iconType === \"custom\") ? this.faClassInactiveCustomIcons : 'fa ' + icons[this.iconType];\n        const $activeIcons = this.$target.find('.s_rating_active_icons > i');\n        const $inactiveIcons = this.$target.find('.s_rating_inactive_icons > i');\n        $activeIcons.removeClass().addClass(faClassActiveIcons);\n        $inactiveIcons.removeClass().addClass(faClassInactiveIcons);\n    },\n});\n", "/** @odoo-module **/\n\nimport { uniqueId } from \"@web/core/utils/functions\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.NavTabs = options.registry.MultipleItems.extend({\n    isTopOption: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        this._findLinksAndPanes();\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    onBuilt: function () {\n        this._generateUniqueIDs();\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this._generateUniqueIDs();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetVisibility: async function (widgetName, params) {\n        if (widgetName === 'remove_tab_opt') {\n            return (this.$tabPanes.length > 2);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _findLinksAndPanes: function () {\n        this.$navLinks = this.$target.find('.nav:first .nav-link');\n        this.$tabPanes = this.$target.find(\".tab-content:first > .tab-pane\");\n    },\n    /**\n     * @private\n     */\n    _generateUniqueIDs: function () {\n        for (var i = 0; i < this.$navLinks.length; i++) {\n            var id = uniqueId(new Date().getTime() + \"_\");\n            var idLink = 'nav_tabs_link_' + id;\n            var idContent = 'nav_tabs_content_' + id;\n            this.$navLinks.eq(i).attr({\n                'id': idLink,\n                'href': '#' + idContent,\n                'aria-controls': idContent,\n            });\n            this.$tabPanes.eq(i).attr({\n                'id': idContent,\n                'aria-labelledby': idLink,\n            });\n        }\n    },\n    /**\n     * @override\n     */\n    _addItemCallback($target) {\n        $target.removeClass('active show');\n        const $targetNavItem = this.$(`.nav-item a[href=\"#${$target.attr('id')}\"]`)\n            .removeClass('active show').parent();\n        const $navLink = $targetNavItem.clone().insertAfter($targetNavItem)\n            .find('.nav-link');\n        this._findLinksAndPanes();\n        this._generateUniqueIDs();\n        new window.Tab($navLink[0]).show();\n    },\n    /**\n     * @override\n     */\n    _removeItemCallback($target) {\n        const $targetNavLink = this.$(`.nav-item a[href=\"#${$target.attr('id')}\"]`);\n        const $navLinkToShow = this.$navLinks.eq((this.$navLinks.index($targetNavLink) + 1) % this.$navLinks.length);\n        $targetNavLink.parent().remove();\n        this._findLinksAndPanes();\n        new window.Tab($navLinkToShow[0]).show();\n    },\n});\noptions.registry.NavTabsStyle = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Manage different tabs styles and their respective classes\n     *\n     * @see this.selectClass for parameters\n     */\n    setStyle(previewMode, widgetValue, params) {\n        // const $nav = this.$target.find('.s_tabs_nav:first .nav');\n        // const isPills = widgetValue === 'pills';\n        // $nav.toggleClass('nav-tabs card-header-tabs', !isPills);\n        // $nav.toggleClass('nav-pills', isPills);\n        // this.$target.find('.s_tabs_nav:first').toggleClass('card-header', !isPills).toggleClass('mb-3', isPills);\n        // this.$target.toggleClass('card', !isPills);\n        // this.$target.find('.s_tabs_content:first').toggleClass('card-body', !isPills);\n        const isTabs = widgetValue === 'nav-tabs';\n        const isBtns = widgetValue === 'nav-buttons';\n\n        const mainEl = this.$target[0];\n        const tabsEl = this.$target[0].querySelector(\".s_tabs_nav\");\n        const navEl = this.$target[0].querySelector(\".s_tabs_nav .nav\");\n        const contentEl = this.$target[0].querySelector(\".s_tabs_content\");\n\n        const tabsTabsClasses = ['card-header', 'px-0', 'border-0', 'overflow-x-auto', 'overflow-y-hidden'];\n        const navTabsClasses = ['card-header-tabs', 'mx-0', 'px-2', 'border-bottom'];\n        const tabsBtnClasses = ['d-flex', 'rounded'];\n        const navBtnClasses = ['d-inline-flex', 'nav-pills', 'p-2'];\n        const tabsPossibleClasses = params.possibleValues.concat(tabsTabsClasses, tabsBtnClasses);\n        const navPossibleClasses = params.possibleValues.concat(navTabsClasses, navBtnClasses);\n\n        // Clean tabsEl from any possible value\n        for (const possibleValue of tabsPossibleClasses) {\n            possibleValue && tabsEl.classList.remove(possibleValue);\n        }\n\n        // Clean navEl from any possible value\n        for (const possibleValue of navPossibleClasses) {\n            possibleValue && navEl.classList.remove(possibleValue);\n        }\n\n        // Apply the new value(s) to tabsEl\n        isTabs && tabsEl.classList.add(...tabsTabsClasses);\n        isBtns && tabsEl.classList.add(...tabsBtnClasses);\n\n        // Apply the new value(s) to navEl\n        widgetValue && navEl.classList.add(widgetValue);\n        isTabs && navEl.classList.add(...navTabsClasses);\n        isBtns && navEl.classList.add(...navBtnClasses);\n\n        // Adapt other elements accordingly\n        mainEl.classList.toggle('card', isTabs);\n        tabsEl.classList.toggle('mb-3', !isTabs);\n        navEl.classList.toggle('overflow-x-auto', !isTabs);\n        navEl.classList.toggle('overflow-y-hidden', !isTabs);\n        contentEl.classList.toggle('p-3', isTabs);\n    },\n    /**\n     * Horizontal/vertical nav.\n     *\n     * @see this.selectClass for parameters\n     */\n    setDirection: function (previewMode, widgetValue, params) {\n        const isVertical = widgetValue === 'vertical';\n        const mainEl = this.$target[0];\n\n        // Toggle classes on the main target\n        mainEl.classList.toggle('row', isVertical);\n        mainEl.classList.toggle('s_col_no_resize', isVertical);\n        mainEl.classList.toggle('s_col_no_bgcolor', isVertical);\n\n        // Select relevant elements within mainEl\n        const nav = mainEl.querySelector('.s_tabs_nav .nav');\n        const navLinks = mainEl.querySelectorAll('.s_tabs_nav > .nav-link');\n        const tabsNav = mainEl.querySelector('.s_tabs_nav');\n        const tabsContent = mainEl.querySelector('.s_tabs_content');\n\n        // Toggle classes based on 'isVertical'\n        nav.classList.toggle('flex-sm-column', isVertical);\n        navLinks.forEach(link => link.classList.toggle('py-2', isVertical));\n        tabsNav.classList.toggle('col-sm-3', isVertical);\n        tabsContent.classList.toggle('col-sm-9', isVertical);\n\n        // Clean leftover classes not needed in vertical mode\n        isVertical && nav.classList.remove('nav-fill', 'nav-justified', 'justify-content-center', 'justify-content-end');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        const navEl = this.$target[0].querySelector(\".s_tabs_nav .nav\");\n\n        switch (methodName) {\n            case 'setStyle':\n                const matchingValue = params.possibleValues.find(value => navEl.classList.contains(value));\n                return matchingValue;\n            case 'setDirection':\n                return this.$target.find('.s_tabs_nav:first .nav').hasClass('flex-sm-column') ? 'vertical' : 'horizontal';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === \"alignment_opt\") {\n            const isFill = this.$target[0].classList.contains(\"nav-fill\");\n            const isJustified = this.$target[0].classList.contains(\"nav-justified\");\n            const isVertical = this.$target[0].classList.contains(\"flex-column\");\n\n            return !(isFill || isJustified || isVertical);\n        }\n        return this._super(...arguments);\n    },\n});\n\n// Prevent `.nav-items` to be deleted from the bin button\n// as it is bypassing the \"add(+)/remove(-)\" behaviour\noptions.registry.TabsNavItems = options.Class.extend({\n    forceNoDeleteButton: true,\n});\n", "/** @odoo-module **/\n\nimport { clamp } from \"@web/core/utils/numbers\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.progress = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Changes the position of the progressbar text.\n     *\n     * @see this.selectClass for parameters\n     */\n    display: function (previewMode, widgetValue, params) {\n        // retro-compatibility\n        if (this.$target.hasClass('progress')) {\n            this.$target.removeClass('progress');\n            this.$target.find('.progress-bar').wrap($('<div/>', {\n                class: 'progress',\n            }));\n            this.$target.find('.progress-bar span').addClass('s_progress_bar_text');\n        }\n\n        const progress = this.$target[0].querySelector(\".progress\");\n        const progressValue = progress.getAttribute(\"aria-valuenow\");\n        let progressLabel = this.$target[0].querySelector('.s_progress_bar_text');\n\n        if (!progressLabel && widgetValue !== 'none') {\n            progressLabel = document.createElement('span');\n            progressLabel.classList.add('s_progress_bar_text', 'small');\n            progressLabel.textContent = progressValue + '%';\n        }\n\n        if (widgetValue === 'inline') {\n            this.$target[0].querySelector('.progress-bar').appendChild(progressLabel);\n        } else if (['below', 'after'].includes(widgetValue)) {\n            progress.insertAdjacentElement('afterend', progressLabel);\n        }\n\n        // Temporary hide the label. It's effectively removed in cleanForSave\n        // if the option is confirmed\n        progressLabel.classList.toggle('d-none', widgetValue === 'none');\n    },\n    /**\n     * Sets the progress bar value.\n     *\n     * @see this.selectClass for parameters\n     */\n    progressBarValue: function (previewMode, widgetValue, params) {\n        let value = parseInt(widgetValue);\n        value = clamp(value, 0, 100);\n        const $progressBar = this.$target.find('.progress-bar');\n        const $progressBarText = this.$target.find('.s_progress_bar_text');\n        const progressMain = this.$target[0].querySelector(\".progress\");\n        // Target precisely the XX% not only XX to not replace wrong element\n        // eg 'Since 1978 we have completed 45%' <- don't replace 1978\n        $progressBarText.text($progressBarText.text().replace(/[0-9]+%/, value + '%'));\n        progressMain.setAttribute('aria-valuenow', value);\n        $progressBar.css(\"width\", value + \"%\");\n    },\n    /**\n     * @override\n     */\n    async cleanForSave() {\n        const progressBar = this.$target[0].querySelector(\".progress-bar\");\n        const progressLabel = this.$target[0].querySelector(\".s_progress_bar_text\");\n\n        if (!progressBar.classList.contains('progress-bar-striped')) {\n            progressBar.classList.remove('progress-bar-animated');\n        }\n\n        if (progressLabel && progressLabel.classList.contains('d-none')) {\n            progressLabel.remove();\n        }\n    },\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'progressBarValue': {\n                return this.$target[0].querySelector(\".progress\").getAttribute(\"aria-valuenow\") + \"%\";\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.TableOfContent = options.Class.extend({\n    /**\n     * @override\n     */\n    start: function () {\n        this.targetedElements = 'h1, h2';\n        this.oldHeadingsEls = [];\n        this.oldHeadingsDesktopVisible = [];\n        const $headings = this.$target.find(this.targetedElements);\n        if ($headings.length > 0) {\n            this._generateNav();\n        }\n        // Generate the navbar if the content changes\n        const targetNode = this.$target.find('.s_table_of_content_main')[0];\n        const config = {attributes: false, childList: true, subtree: true, characterData: true};\n        this.observer = new MutationObserver(() => this._generateNav());\n        this.observer.observe(targetNode, config);\n        // The mutation observer doesn't observe the attributes change, it would\n        // be too much. Adding content_changed \"listener\" instead.\n        this.$target.on('content_changed', () => this._generateNav());\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        // The observer needs to be disconnected first.\n        this.observer.disconnect();\n        this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onRemove() {\n        this._disposeScrollSpy();\n        const exception = (tocEl) => tocEl === this.$target[0];\n        this._activateScrollSpy(exception);\n    },\n    /**\n     * @override\n     */\n    onClone: function () {\n        this._generateNav();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @param  {Function} exception\n     */\n    _activateScrollSpy(exception) {\n        for (const tocEl of this.ownerDocument.querySelectorAll('#wrapwrap .s_table_of_content')) {\n            if (exception(tocEl)) {\n                continue;\n            }\n            this.trigger_up('widgets_start_request', {\n                $target: $(tocEl),\n                editableMode: true,\n            });\n        }\n    },\n    /**\n     * @private\n     */\n    _disposeScrollSpy() {\n        const scrollingEl = $().getScrollingElement(this.ownerDocument)[0];\n        const scrollSpyInstance =\n            this.$target[0].ownerDocument.defaultView.ScrollSpy.getInstance(scrollingEl);\n        if (scrollSpyInstance) {\n            scrollSpyInstance.dispose();\n        }\n    },\n    /**\n     * Returns the TOC id and the heading id from a header element.\n     *\n     * @param {HTMLElement} headingEl - A header element of the TOC.\n     * @returns {Object}\n     */\n    _getTocAndHeadingId(headingEl) {\n        const match = /^table_of_content_heading_(\\d+)_(\\d+)$/.exec(headingEl.getAttribute(\"id\"));\n        if (match) {\n            return { tocId: parseInt(match[1]), headingId: parseInt(match[2]) };\n        }\n        return { tocId: 0, headingId: 0 };\n    },\n    /**\n     * @private\n     */\n    _generateNav: function (ev) {\n        const blockTextContent = this.$target[0].textContent.replaceAll('\\n', '').trim();\n        if (blockTextContent === '') {\n            // destroy public widget and remove the ToC since there are no more\n            // child elements, before doing so the observer needs to be\n            // disconnected else observer observe mutation and _generateNav\n            // gets called even after there's no more ToC.\n            this.observer.disconnect();\n            this.trigger_up('remove_snippet', {$snippet: this.$target});\n            return;\n        }\n        this.options.wysiwyg && this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n        const navEl = this.$target[0].querySelector('.s_table_of_content_navbar');\n        const headingsEls = this.$target.find(this.targetedElements).toArray();\n        const areHeadingsEqual = this.oldHeadingsEls.length === headingsEls.length\n            && this.oldHeadingsEls.every((el, i) =>\n                el.isEqualNode(headingsEls[i])\n                && this.oldHeadingsDesktopVisible[i] === !headingsEls[i].closest(\".o_snippet_desktop_invisible\")\n            );\n        const areVisibilityIdsEqual = headingsEls.every((headingEl) => {\n            const visibilityId = headingEl.closest('section').getAttribute('data-visibility-id');\n            const matchingLinkEl = navEl.querySelector(`a[href=\"#${headingEl.getAttribute('id')}\"]`);\n            const matchingLinkVisibilityId = matchingLinkEl ? matchingLinkEl.getAttribute('data-visibility-id') : null;\n            // Check if visibilityId matches matchingLinkVisibilityId or both\n            // are null/undefined\n            return visibilityId === matchingLinkVisibilityId;\n        });\n        if (areHeadingsEqual && areVisibilityIdsEqual) {\n            // If the content of the navbar before the change of the DOM is\n            // equal to the content of the navbar after the change of the DOM,\n            // then there is no need to regenerate the navbar.\n            // This is especially important as to regenerate it, we also have\n            // to restart scrollSpy, which is done by restarting widgets. But\n            // restarting all widgets inside the ToC would certainly lead to\n            // DOM changes... which would then regenerate the navbar and lead to\n            // an infinite loop.\n            return;\n        }\n        // We dispose the scrollSpy because the navbar will be updated.\n        this._disposeScrollSpy();\n\n        const firstHeadingEl = headingsEls[0];\n        let tocId = firstHeadingEl ? this._getTocAndHeadingId(firstHeadingEl).tocId : 0;\n        const tocEls = this.$target[0].ownerDocument.body.querySelectorAll(\"[data-snippet='s_table_of_content']\");\n        const otherTocEls = [...tocEls].filter(tocEl => tocEl !== this.$target[0]);\n        const otherTocIds = otherTocEls.map(tocEl => {\n            const firstHeadingEl = tocEl.querySelector(this.targetedElements);\n            return this._getTocAndHeadingId(firstHeadingEl).tocId;\n        });\n        if (!tocId || otherTocIds.includes(tocId)) {\n            tocId = 1 + Math.max(0, ...otherTocIds);\n        }\n        const headingIds = headingsEls.map(headingEl => this._getTocAndHeadingId(headingEl).headingId);\n        let maxHeadingIds = Math.max(0, ...headingIds);\n\n        navEl.innerHTML = '';\n        const uniqueHeadingIds = new Set();\n        headingsEls.forEach((el) => {\n            const $el = $(el);\n            let headingId = this._getTocAndHeadingId(el).headingId;\n            if (headingId) {\n                // Reset headingId on duplicate.\n                if (uniqueHeadingIds.has(headingId)) {\n                    headingId = 0;\n                } else {\n                    uniqueHeadingIds.add(headingId);\n                }\n            }\n            if (!headingId) {\n                maxHeadingIds += 1;\n                headingId = maxHeadingIds;\n            }\n            // Generate stable ids so that external links to heading anchors do\n            // not get broken next time the navigation links are re-generated.\n            const id = `table_of_content_heading_${tocId}_${headingId}`;\n            $el.attr('id', id);\n            if (!el.closest('.o_snippet_desktop_invisible')) {\n                // Generate navigation entry only for desktop.\n                const visibilityId = $el.closest('section').attr('data-visibility-id');\n                $('<a>').attr({ 'href': \"#\" + id, 'data-visibility-id': visibilityId })\n                        .addClass('table_of_content_link list-group-item list-group-item-action py-2 border-0 rounded-0')\n                        .text($el.text())\n                        .appendTo(navEl);\n                $el[0].dataset.anchor = 'true';\n            }\n        });\n        const exception = (tocEl) => !tocEl.querySelector('.s_table_of_content_navbar a');\n        this._activateScrollSpy(exception);\n        this.oldHeadingsEls = [...headingsEls.map(el => el.cloneNode(true))];\n        this.oldHeadingsDesktopVisible = [...headingsEls.map(el => !el.closest('.o_snippet_desktop_invisible'))];\n    },\n});\n\noptions.registry.TableOfContentNavbar = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Change the navbar position.\n     *\n     * @see this.selectClass for parameters\n     */\n    navbarPosition: function (previewMode, widgetValue, params) {\n        const $navbar = this.$target;\n        const $mainContent = this.$target.parent().find('.s_table_of_content_main');\n        if (widgetValue === 'top' || widgetValue === 'left') {\n            $navbar.prev().before($navbar);\n        }\n        if (widgetValue === 'left' || widgetValue === 'right') {\n            $navbar.removeClass('s_table_of_content_horizontal_navbar col-lg-12').addClass('s_table_of_content_vertical_navbar col-lg-3');\n            $mainContent.removeClass('col-lg-12').addClass('col-lg-9');\n            $navbar.find('.s_table_of_content_navbar').removeClass('list-group-horizontal-md');\n        }\n        if (widgetValue === 'right') {\n            $navbar.next().after($navbar);\n        }\n        if (widgetValue === 'top') {\n            $navbar.removeClass('s_table_of_content_vertical_navbar col-lg-3').addClass('s_table_of_content_horizontal_navbar col-lg-12');\n            $navbar.find('.s_table_of_content_navbar').addClass('list-group-horizontal-md');\n            $mainContent.removeClass('col-lg-9').addClass('col-lg-12');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'navbarPosition': {\n                const $navbar = this.$target;\n                if ($navbar.hasClass('s_table_of_content_horizontal_navbar')) {\n                    return 'top';\n                } else {\n                    const $mainContent = $navbar.parent().find('.s_table_of_content_main');\n                    return $navbar.prev().is($mainContent) === true ? 'right' : 'left';\n                }\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n\noptions.registry.TableOfContentMainColumns = options.Class.extend({\n    forceNoDeleteButton: true,\n});\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.Timeline = options.Class.extend({\n    displayOverlayOptions: true,\n\n    /**\n     * @override\n     */\n    start: function () {\n        var $buttons = this.$el.find('we-button.o_we_overlay_opt');\n        var $overlayArea = this.$overlay.find('.o_overlay_options_wrap');\n        $overlayArea.append($buttons);\n\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Moves the card to the right/left.\n     *\n     * @see this.selectClass for parameters\n     */\n    timelineCard(previewMode, widgetValue, params) {\n        const timelineRowEl = this.$target[0].closest(\".s_timeline_row\");\n        const timelineCardEls = timelineRowEl.querySelectorAll(\".s_timeline_card\");\n        const firstContentEl = timelineRowEl.querySelector(\".s_timeline_content\");\n        timelineRowEl.append(firstContentEl);\n        timelineCardEls.forEach(card => card.classList.toggle(\"text-md-end\"));\n    },\n});\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.MediaItemLayout = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Change the media item layout.\n     *\n     * @see this.selectClass for parameters\n     */\n    layout: function (previewMode, widgetValue, params) {\n        const $image = this.$target.find('.s_media_list_img_wrapper');\n        const $content = this.$target.find('.s_media_list_body');\n\n        for (const possibleValue of params.possibleValues) {\n            $image.removeClass(`col-lg-${possibleValue}`);\n            $content.removeClass(`col-lg-${12 - possibleValue}`);\n        }\n        $image.addClass(`col-lg-${widgetValue}`);\n        $content.addClass(`col-lg-${12 - widgetValue}`);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'layout': {\n                const $image = this.$target.find('.s_media_list_img_wrapper');\n                for (const possibleValue of params.possibleValues) {\n                    if ($image.hasClass(`col-lg-${possibleValue}`)) {\n                        return possibleValue;\n                    }\n                }\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport options from \"@web_editor/js/editor/snippets.options\";\n\noptions.registry.GoogleMap = options.Class.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    resetMapColor(previewMode, widgetValue, params) {\n        this.$target[0].dataset.mapColor = '';\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setFormattedAddress(previewMode, widgetValue, params) {\n        this.$target[0].dataset.pinAddress = params.gmapPlace.formatted_address;\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async showDescription(previewMode, widgetValue, params) {\n        const descriptionEl = this.$target[0].querySelector('.description');\n        if (widgetValue && !descriptionEl) {\n            this.$target.append($(`\n                <div class=\"description\">\n                    <font>${_t('Visit us:')}</font>\n                    <span>${_t('Our office is located in the northeast of Brussels. TEL (555) 432 2365')}</span>\n                </div>`)\n            );\n        } else if (!widgetValue && descriptionEl) {\n            descriptionEl.remove();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'showDescription') {\n            return this.$target[0].querySelector('.description') ? 'true' : '';\n        }\n        return this._super(...arguments);\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport options from '@web_editor/js/editor/snippets.options';\nimport {generateGMapIframe, generateGMapLink} from '@website/js/utils';\n\noptions.registry.Map = options.Class.extend({\n    /**\n     * @override\n     */\n    onBuilt() {\n        // The iframe is added here to the snippet when it is dropped onto the\n        // page. However, in the case where a custom snippet saved by the user\n        // is dropped, the iframe already exists and doesn't need to be added\n        // again.\n        if (!this.$target[0].querySelector('.s_map_embedded')) {\n            const iframeEl = generateGMapIframe();\n            this.$target[0].querySelector('.s_map_color_filter').before(iframeEl);\n            this._updateSource();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @see this.selectClass for parameters\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (['mapAddress', 'mapType', 'mapZoom'].includes(params.attributeName)) {\n            this._updateSource();\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    async showDescription(previewMode, widgetValue, params) {\n        const descriptionEl = this.$target[0].querySelector('.description');\n        if (widgetValue && !descriptionEl) {\n            this.$target.append($(`\n                <div class=\"description\">\n                    <font>${_t('Visit us:')}</font>\n                    <span>${_t('Our office is open Monday \u2013 Friday 8:30 a.m. \u2013 4:00 p.m.')}</span>\n                </div>`)\n            );\n        } else if (!widgetValue && descriptionEl) {\n            descriptionEl.remove();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === 'showDescription') {\n            return !!this.$target[0].querySelector('.description');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @private\n     */\n    _updateSource() {\n        const dataset = this.$target[0].dataset;\n        const $embedded = this.$target.find('.s_map_embedded');\n        const $info = this.$target.find('.missing_option_warning');\n        if (dataset.mapAddress) {\n            const url = generateGMapLink(dataset);\n            if (url !== $embedded.attr('src')) {\n                $embedded.attr('src', url);\n            }\n            $embedded.removeClass('d-none');\n            $info.addClass('d-none');\n        } else {\n            $embedded.attr('src', 'about:blank');\n            $embedded.addClass('d-none');\n            $info.removeClass('d-none');\n        }\n    },\n});\n\nexport default {\n    Map: options.registry.Map,\n};\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst dynamicSnippetOptions = options.Class.extend({\n    /**\n     * This type defines the template infos retrieved from\n     * @see /website/snippet/filter_templates\n     * Used for\n     * @see this.dynamicFilterTemplates\n     * @typedef {Object} Template - definition of a dynamic snippet template\n     * @property {string} key - key of the template\n     * @property {string} numOfEl - number of elements on desktop\n     * @property {string} numOfElSm - number of elements on mobile\n     * @property {string} numOfElFetch - number of elements to fetch\n     * @property {string} rowPerSlide - number of rows per slide\n     * @property {string} arrowPosition - position of the arrows\n     * @property {string} extraClasses - classes to be added to the <section>\n     */\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        // specify model name in subclasses to filter the list of available model record filters\n        this.modelNameFilter = undefined;\n        this.contextualFilterDomain = [];\n        this.dynamicFilters = {};\n        // name of the model of the currently selected filter, used to fetch templates\n        this.currentModelName = undefined;\n        /** @type {Object.<string, Template>} - key is the key of the template */\n        this.dynamicFilterTemplates = {};\n        // Indicates that some current options are a default selection.\n        this.isOptionDefault = {};\n    },\n    /**\n     * @override\n     */\n    async willStart() {\n        const _super = this._super.bind(this);\n        await this._fetchDynamicFilters();\n        await this._fetchDynamicFilterTemplates();\n        return _super(...arguments);\n    },\n    /**\n     *\n     * @override\n     */\n    async onBuilt() {\n        // Default values depend on the templates and filters available.\n        // Therefore, they cannot be computed prior the start of the option.\n        await this._setOptionsDefaultValues();\n        // The target needs to be restarted when the correct\n        // template values are applied (numberOfElements, rowPerSlide, etc.)\n        return this._refreshPublicWidgets();\n    },\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        this.customTemplateData = JSON.parse(this.$target[0].dataset?.customTemplateData || \"{}\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     *\n     * @see this.selectClass for parameters\n     */\n    selectDataAttribute: function (previewMode, widgetValue, params) {\n        this._super.apply(this, arguments);\n        if (params.attributeName === 'filterId' && previewMode === false) {\n            const filter = this.dynamicFilters[parseInt(widgetValue)];\n            this.$target.get(0).dataset.numberOfRecords = filter.limit;\n            return this._filterUpdated(filter);\n        }\n        if (params.attributeName === 'templateKey' && previewMode === false) {\n            this._templateUpdated(widgetValue, params.activeValue);\n        }\n    },\n    /**\n     * Saves the template data that will be handled later by the public widget.\n     *\n     * @see this.selectClass for parameters\n     */\n    customizeTemplateValues(previewMode, widgetValue, params) {\n        this.customTemplateData[params.customizeTemplateKey] = widgetValue === \"true\";\n        this.$target[0].dataset.customTemplateData = JSON.stringify(this.customTemplateData);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * See from updateUI in s_website_form\n     *\n     * @override\n     */\n    async updateUI() {\n        if (this.rerender) {\n            this.rerender = false;\n            await this._rerenderXML();\n            return;\n        }\n        await this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @returns {Template}\n     */\n    _getCurrentTemplate: function () {\n        return this.dynamicFilterTemplates[this.$target.get(0).dataset['templateKey']];\n    },\n\n    _getTemplateClass: function (templateKey) {\n        return templateKey.replace(/.*\\.dynamic_filter_template_/, \"s_\");\n    },\n\n    /**\n     *\n     * @override\n     * @private\n     */\n    _computeWidgetVisibility: function (widgetName, params) {\n        if (widgetName === 'filter_opt') {\n            // Hide if exaclty one is available: show when none to help understand what is missing\n            return Object.keys(this.dynamicFilters).length !== 1;\n        }\n\n        if (widgetName === 'number_of_records_opt') {\n            const template = this._getCurrentTemplate();\n            return template && !template.numOfElFetch;\n        }\n\n        return this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     * @private\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === \"customizeTemplateValues\") {\n            return `${this.customTemplateData[params.customizeTemplateKey] || false}`;\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     * @private\n     * @returns {Promise}\n     */\n    _refreshPublicWidgets: function () {\n        return this._super.apply(this, arguments).then(() => {\n            const template = this._getCurrentTemplate();\n            this.$target.find('.missing_option_warning').toggleClass(\n                'd-none',\n                !!template\n            );\n        });\n    },\n    /**\n     * Fetches dynamic filters and set them in {@link this.dynamicFilters}.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async _fetchDynamicFilters() {\n        const dynamicFilters = await rpc('/website/snippet/options_filters', {\n            model_name: this.modelNameFilter,\n            search_domain: this.contextualFilterDomain,\n        });\n        if (!dynamicFilters.length) {\n            // Additional modules are needed for dynamic filters to be defined.\n            return;\n        }\n        for (let index in dynamicFilters) {\n            this.dynamicFilters[dynamicFilters[index].id] = dynamicFilters[index];\n        }\n        this._defaultFilterId = dynamicFilters[0].id;\n    },\n    /**\n     * Fetch dynamic filters templates and set them  in {@link this.dynamicFilterTemplates}.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async _fetchDynamicFilterTemplates() {\n        const filter = this.dynamicFilters[this.$target.get(0).dataset['filterId']] || this.dynamicFilters[this._defaultFilterId];\n        this.dynamicFilterTemplates = {};\n        if (!filter) {\n            return [];\n        }\n        const dynamicFilterTemplates = await rpc('/website/snippet/filter_templates', {\n            filter_name: filter.model_name.replaceAll('.', '_'),\n        });\n        for (let index in dynamicFilterTemplates) {\n            this.dynamicFilterTemplates[dynamicFilterTemplates[index].key] = dynamicFilterTemplates[index];\n        }\n        this._defaultTemplateKey = dynamicFilterTemplates[0].key;\n    },\n    /**\n     *\n     * @override\n     * @private\n     */\n    _renderCustomXML: async function (uiFragment) {\n        await this._renderDynamicFiltersSelector(uiFragment);\n        await this._renderDynamicFilterTemplatesSelector(uiFragment);\n    },\n    /**\n     * Renders the dynamic filter option selector content into the provided uiFragment.\n     * @param {HTMLElement} uiFragment\n     * @private\n     */\n    _renderDynamicFiltersSelector: async function (uiFragment) {\n        const filtersSelectorEl = uiFragment.querySelector('[data-name=\"filter_opt\"]');\n        return this._renderSelectUserValueWidgetButtons(filtersSelectorEl, this.dynamicFilters);\n    },\n    /**\n     * Renders we-buttons into a SelectUserValueWidget element according to provided data.\n     * @param {HTMLElement} selectUserValueWidgetElement the SelectUserValueWidget buttons\n     *   have to be created into.\n     * @param {Object} data\n     * @private\n     */\n    _renderSelectUserValueWidgetButtons: async function (selectUserValueWidgetElement, data) {\n        for (let id in data) {\n            const button = document.createElement('we-button');\n            button.dataset.selectDataAttribute = id;\n            if (data[id].thumb) {\n                button.dataset.img = data[id].thumb;\n            } else {\n                button.innerText = data[id].name;\n            }\n            selectUserValueWidgetElement.appendChild(button);\n        }\n    },\n    /**\n     * Renders the template option selector content into the provided uiFragment.\n     * @param {HTMLElement} uiFragment\n     * @private\n     */\n    _renderDynamicFilterTemplatesSelector: async function (uiFragment) {\n        const templatesSelectorEl = uiFragment.querySelector('[data-name=\"template_opt\"]');\n        return this._renderSelectUserValueWidgetButtons(templatesSelectorEl, this.dynamicFilterTemplates);\n    },\n    /**\n     * Sets default options values.\n     * Method to be overridden in child components in order to set additional\n     * options default values.\n     * @private\n     */\n    async _setOptionsDefaultValues() {\n        // Unactive the editor observer, otherwise, undo of the editor will undo\n        // the attribute being changed. In some case of undo, a race condition\n        // with the public widget that use following property (eg.\n        // numberOfElements or numberOfElementsSmallDevices) might throw an\n        // exception by not finding the attribute on the element.\n        this.options.wysiwyg.odooEditor.observerUnactive();\n        const filterKeys = this.$el.find(\"we-select[data-attribute-name='filterId'] we-selection-items we-button\");\n        if (filterKeys.length > 0) {\n            this._setOptionValue('numberOfRecords', this.dynamicFilters[Object.keys(this.dynamicFilters)[0]].limit);\n        }\n        let selectedFilterId = this.$target.get(0).dataset['filterId'];\n        if (Object.keys(this.dynamicFilters).length > 0) {\n            if (!this.dynamicFilters[selectedFilterId]) {\n                this.$target.get(0).dataset['filterId'] = this._defaultFilterId;\n                this.isOptionDefault['filterId'] = true;\n                selectedFilterId = this._defaultFilterId;\n            }\n        }\n        if (this.dynamicFilters[selectedFilterId] &&\n                !this.dynamicFilterTemplates[this.$target.get(0).dataset['templateKey']]) {\n            this._setDefaultTemplate();\n        }\n        this.options.wysiwyg.odooEditor.observerActive();\n    },\n    /**\n     * Take the new filter selection into account\n     * @param filter\n     * @private\n     */\n    async _filterUpdated(filter) {\n        if (filter && this.currentModelName !== filter.model_name) {\n            this.currentModelName = filter.model_name;\n            await this._fetchDynamicFilterTemplates();\n            if (Object.keys(this.dynamicFilterTemplates).length > 0) {\n                const selectedTemplateId = this.$target.get(0).dataset['templateKey'];\n                if (!this.dynamicFilterTemplates[selectedTemplateId]) {\n                    this._setDefaultTemplate();\n                }\n            }\n            this.rerender = true;\n        }\n    },\n    /**\n     * Sets the default filter template.\n     * @private\n     */\n    _setDefaultTemplate() {\n        if (Object.keys(this.dynamicFilterTemplates).length) {\n            this.$target.get(0).dataset['templateKey'] = this._defaultTemplateKey;\n            this.isOptionDefault['templateKey'] = true;\n            this._templateUpdated(this._defaultTemplateKey);\n        }\n    },\n\n    /**\n     * Take the new template selection into account\n     * @param {String} newTemplateKey\n     * @param {String} [oldTemplateKey]\n     * @private\n     */\n    _templateUpdated(newTemplateKey, oldTemplateKey) {\n        if (oldTemplateKey) {\n            this.$target.removeClass(this._getTemplateClass(oldTemplateKey));\n        }\n        this.$target.addClass(this._getTemplateClass(newTemplateKey));\n\n        const template = this.dynamicFilterTemplates[newTemplateKey];\n        if (template.numOfEl) {\n            this.$target[0].dataset.numberOfElements = template.numOfEl;\n        } else {\n            delete this.$target[0].dataset.numberOfElements;\n        }\n        if (template.numOfElSm) {\n            this.$target[0].dataset.numberOfElementsSmallDevices = template.numOfElSm;\n        } else {\n            delete this.$target[0].dataset.numberOfElementsSmallDevices;\n        }\n        if (template.numOfElFetch) {\n            this.$target[0].dataset.numberOfRecords = template.numOfElFetch;\n        }\n        if (template.extraClasses) {\n            this.$target[0].dataset.extraClasses = template.extraClasses;\n        } else {\n            delete this.$target[0].dataset.extraClasses;\n        }\n        if (template.columnClasses) {\n            this.$target[0].dataset.columnClasses = template.columnClasses;\n        } else {\n            delete this.$target[0].dataset.columnClasses;\n        }\n    },\n    /**\n     * Sets the option value.\n     * @param optionName\n     * @param value\n     * @private\n     */\n    _setOptionValue: function (optionName, value) {\n        const selectedTemplateId = this.$target.get(0).dataset['templateKey'];\n        if (this.$target.get(0).dataset[optionName] === undefined || this.isOptionDefault[optionName]) {\n            this.$target.get(0).dataset[optionName] = value;\n            this.isOptionDefault[optionName] = false;\n        }\n        if (optionName === 'templateKey') {\n            this._templateUpdated(value, selectedTemplateId);\n        }\n    },\n});\n\noptions.registry.dynamic_snippet = dynamicSnippetOptions;\noptions.registry.DynamicSnippetTitle = options.Class.extend({\n    forceNoDeleteButton: true,\n});\n\nexport default dynamicSnippetOptions;\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport s_dynamic_snippet_options from \"@website/snippets/s_dynamic_snippet/options\";\n\nconst dynamicSnippetCarouselOptions = s_dynamic_snippet_options.extend({\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     *\n     * @override\n     * @private\n     */\n    _setOptionsDefaultValues: function () {\n        this._super.apply(this, arguments);\n        this._setOptionValue('carouselInterval', '5000');\n    },\n    /**\n     * Take the new template selection into account\n     *\n     * @param {number} newTemplate id of the newly selected template\n     * @param {number} oldTemplate id of the previously selected template\n     * @override\n     */\n    _templateUpdated(newTemplate, oldTemplate) {\n        this._super(...arguments);\n        const template = this.dynamicFilterTemplates[newTemplate];\n        if (template.rowPerSlide) {\n            this.$target[0].dataset.rowPerSlide = template.rowPerSlide;\n        } else {\n            delete this.$target[0].dataset.rowPerSlide;\n        }\n        if (template.arrowPosition) {\n            this.$target[0].dataset.arrowPosition = template.arrowPosition;\n        } else {\n            delete this.$target[0].dataset.arrowPosition;\n        }\n    },\n\n});\n\noptions.registry.dynamic_snippet_carousel = dynamicSnippetCarouselOptions;\n\nexport default dynamicSnippetCarouselOptions;\n", "/** @odoo-module **/\n\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport { rpc } from \"@web/core/network/rpc\";\n\nconst mainObjectRe = /website\\.controller\\.page\\(((\\d+,?)*)\\)/;\n\noptions.registry.WebsiteControllerPageListingLayout = options.Class.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n        this.resModel = \"website.controller.page\";\n    },\n\n    /**\n     * @override\n     */\n    async willStart() {\n        const _super = this._super.bind(this);\n        const mainObjectRepr = this.$target[0].ownerDocument.documentElement.getAttribute(\"data-main-object\");\n        const match = mainObjectRe.exec(mainObjectRepr);\n        if (match && match[1]) {\n            this.resIds = match[1].split(\",\").flatMap(e => {\n                if (!e) {\n                    return [];\n                }\n                const id = parseInt(e);\n                return id ? [id] : [];\n            });\n        }\n\n        const results = await this.orm.read(this.resModel, this.resIds, [\"default_layout\"]);\n        this.layout = results[0][\"default_layout\"];\n        return _super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    async setLayout(previewMode, widgetValue) {\n        const params = {\n            layout_mode: widgetValue,\n            view_id: this.$target[0].getAttribute(\"data-view-id\"),\n        };\n        // save the default layout display, and set the layout for the current user\n        await Promise.all([\n            this.orm.write(this.resModel, this.resIds, { default_layout: widgetValue }),\n            rpc(\"/website/save_session_layout_mode\", params),\n        ]);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n    *\n    * @param methodName\n    * @param params\n    * @returns {string|string|*}\n    * @private\n    */\n   _computeWidgetState(methodName) {\n        switch (methodName) {\n            case 'setLayout': {\n                return this.layout;\n            }\n        }\n        return this._super(...arguments);\n   },\n});\n", "/** @odoo-module **/\n\nimport FormEditorRegistry from \"@website/js/form_editor_registry\";\nimport options from \"@web_editor/js/editor/snippets.options\";\nimport { ConfirmationDialog } from \"@web/core/confirmation_dialog/confirmation_dialog\";\nimport weUtils from \"@web_editor/js/common/utils\";\nimport \"@website/js/editor/snippets.options\";\nimport { unique } from \"@web/core/utils/arrays\";\nimport { redirect } from \"@web/core/utils/urls\";\nimport { _t } from \"@web/core/l10n/translation\";\nimport { memoize } from \"@web/core/utils/functions\";\nimport { renderToElement } from \"@web/core/utils/render\";\nimport { formatDate, formatDateTime } from \"@web/core/l10n/dates\";\nimport wUtils from '@website/js/utils';\n\nlet currentActionName;\n\nconst allFormsInfo = new Map();\nconst clearAllFormsInfo = () => {\n    allFormsInfo.clear();\n};\n/**\n * Returns the domain of a field.\n *\n * @private\n * @param {HTMLElement} formEl\n * @param {String} name\n * @param {String} type\n * @param {String} relation\n * @returns {Object|false}\n */\nfunction _getDomain(formEl, name, type, relation) {\n    // We need this because the field domain is in formInfo in the\n    // WebsiteFormEditor but we need it in the WebsiteFieldEditor.\n    if (!allFormsInfo.get(formEl) || !name || !type || !relation) {\n        return false;\n    }\n    const field = allFormsInfo.get(formEl).fields\n        .find(el => el.name === name && el.type === type && el.relation === relation);\n    return field && field.domain;\n}\n\nconst authorizedFieldsCache = {\n    data: {},\n    /**\n     * Returns the fields definitions for a form\n     *\n     * @param {HTMLElement} formEl\n     * @param {Object} orm\n     * @returns {Promise}\n     */\n    get(formEl, orm) {\n        // Combine model and fields into cache key.\n        const model = formEl.dataset.model_name;\n        const propertyOrigins = {};\n        const parts = [model];\n        for (const hiddenInputEl of [...formEl.querySelectorAll(\"input[type=hidden]\")].sort(\n            (firstEl, secondEl) => firstEl.name.localeCompare(secondEl.name)\n        )) {\n            // Pushing using the name order to avoid being impacted by the\n            // order of hidden fields within the DOM.\n            parts.push(hiddenInputEl.name);\n            parts.push(hiddenInputEl.value);\n            propertyOrigins[hiddenInputEl.name] = hiddenInputEl.value;\n        }\n        const cacheKey = parts.join(\"/\");\n        if (!(cacheKey in this.data)) {\n            this.data[cacheKey] = orm.call(\"ir.model\", \"get_authorized_fields\", [\n                model,\n                propertyOrigins,\n            ]);\n        }\n        return this.data[cacheKey];\n    },\n};\n\n\nconst FormEditor = options.Class.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    //----------------------------------------------------------------------\n    // Private\n    //----------------------------------------------------------------------\n\n    /**\n     * Returns a promise which is resolved once the records of the field\n     * have been retrieved.\n     *\n     * @private\n     * @param {Object} field\n     * @returns {Promise<Object>}\n     */\n    _fetchFieldRecords: async function (field) {\n        // Convert the required boolean to a value directly usable\n        // in qweb js to avoid duplicating this in the templates\n        field.required = field.required ? 1 : null;\n\n        if (field.records) {\n            return field.records;\n        }\n        if (field._property && field.type === \"tags\") {\n            // Convert tags to records to avoid added complexity.\n            // Tag ids need to escape \",\" to be able to recover their value on\n            // the server side if they contain \",\".\n            field.records = field.tags.map(tag => ({\n                id: tag[0].replaceAll(\"\\\\\", \"\\\\/\").replaceAll(\",\", \"\\\\,\"),\n                display_name: tag[1],\n            }));\n        } else if (field._property && field.comodel) {\n            field.records = await this.orm.searchRead(field.comodel, field.domain || [], [\"display_name\"]);\n        } else if (field.type === \"selection\") {\n            // Set selection as records to avoid added complexity.\n            field.records = field.selection.map(el => ({\n                id: el[0],\n                display_name: el[1],\n            }));\n        } else if (field.relation && field.relation !== 'ir.attachment') {\n            const fieldNames = field.fieldName ? [field.fieldName] : [\"display_name\"];\n            field.records = await this.orm.searchRead(field.relation, field.domain || [], fieldNames);\n            if (field.fieldName) {\n                field.records.forEach(r => r[\"display_name\"] = r[field.fieldName]);\n            }\n        }\n        return field.records;\n    },\n    /**\n     * Returns a field object\n     *\n     * @private\n     * @param {string} type the type of the field\n     * @param {string} name The name of the field used also as label\n     * @returns {Object}\n     */\n    _getCustomField: function (type, name) {\n        return {\n            name: name,\n            string: name,\n            custom: true,\n            type: type,\n            // Default values for x2many fields and selection\n            records: [{\n                id: _t('Option 1'),\n                display_name: _t('Option 1'),\n            }, {\n                id: _t('Option 2'),\n                display_name: _t('Option 2'),\n            }, {\n                id: _t('Option 3'),\n                display_name: _t('Option 3'),\n            }],\n        };\n    },\n    /**\n     * Returns the default formatInfos of a field.\n     *\n     * @private\n     * @returns {Object}\n     */\n    _getDefaultFormat: function () {\n        return {\n            labelWidth: this.$target[0].querySelector('.s_website_form_label')?.style.width || \"200px\",\n            labelPosition: 'left',\n            multiPosition: 'horizontal',\n            requiredMark: this._isRequiredMark(),\n            optionalMark: this._isOptionalMark(),\n            mark: this._getMark(),\n        };\n    },\n    /**\n     * @private\n     * @returns {string}\n     */\n    _getMark: function () {\n        return this.$target[0].dataset.mark;\n    },\n    /**\n     * Replace all `\"` character by `&quot;`.\n     *\n     * @param {string} name\n     * @returns {string}\n     */\n    _getQuotesEncodedName(name) {\n        // Browsers seem to be encoding the double quotation mark character as\n        // `%22` (URI encoded version) when used inside an input's name. It is\n        // actually quite weird as a sent `<input name='Hello \"world\" %22'/>`\n        // will actually be received as `Hello %22world%22 %22` on the server,\n        // making it impossible to know which is actually a real double\n        // quotation mark and not the \"%22\" string. Values do not have this\n        // problem: `Hello \"world\" %22` would be received as-is on the server.\n        // In the future, we should consider not using label values as input\n        // names anyway; the idea was bad in the first place. We should probably\n        // assign random field names (as we do for IDs) and send a mapping\n        // with the labels, as values (TODO ?).\n        return name.replaceAll(/\"/g, character => `&quot;`);\n    },\n    /**\n     * @private\n     * @returns {boolean}\n     */\n    _isOptionalMark: function () {\n        return this.$target[0].classList.contains('o_mark_optional');\n    },\n    /**\n     * @private\n     * @returns {boolean}\n     */\n    _isRequiredMark: function () {\n        return this.$target[0].classList.contains('o_mark_required');\n    },\n    /**\n     * @private\n     * @param {Object} field\n     * @returns {HTMLElement}\n     */\n    _renderField: function (field, resetId = false) {\n        if (!field.id) {\n            field.id = weUtils.generateHTMLId();\n        }\n        const params = { field: { ...field } };\n        if ([\"url\", \"email\", \"tel\"].includes(field.type)) {\n            params.field.inputType = field.type;\n        }\n        if ([\"boolean\", \"selection\", \"binary\"].includes(field.type)) {\n            params.field.isCheck = true;\n        }\n        if (field.type === \"one2many\" && field.relation !== \"ir.attachment\") {\n            params.field.isCheck = true;\n        }\n        if (field.custom && !field.string) {\n            params.field.string = field.name;\n        }\n        if (field.description) {\n            params.default_description = _t(\"Describe your field here.\");\n        } else if ([\"email_cc\", \"email_to\"].includes(field.name)) {\n            params.default_description = _t(\"Separate email addresses with a comma.\");\n        }\n        const template = document.createElement('template');\n        const renderType = field.type === \"tags\" ? \"many2many\" : field.type;\n        template.content.append(renderToElement(\"website.form_field_\" + renderType, params));\n        if (field.description && field.description !== true) {\n            $(template.content.querySelector('.s_website_form_field_description')).replaceWith(field.description);\n        }\n        template.content.querySelectorAll('input.datetimepicker-input').forEach(el => el.value = field.propertyValue);\n        template.content.querySelectorAll(\"[name]\").forEach(el => {\n            el.name = this._getQuotesEncodedName(el.name);\n        });\n        template.content.querySelectorAll(\"[data-name]\").forEach(el => {\n            el.dataset.name = this._getQuotesEncodedName(el.dataset.name);\n        });\n        return template.content.firstElementChild;\n    },\n});\n\nconst FieldEditor = FormEditor.extend({\n    VISIBILITY_DATASET: ['visibilityDependency', 'visibilityCondition', 'visibilityComparator', 'visibilityBetween'],\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this.formEl = this.$target[0].closest('form');\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns the target as a field Object\n     *\n     * @private\n     * @param {boolean} noRecords\n     * @returns {Object}\n     */\n    _getActiveField: function (noRecords) {\n        let field;\n        const labelText = this.$target.find('.s_website_form_label_content').text();\n        if (this._isFieldCustom()) {\n            field = this._getCustomField(this.$target[0].dataset.type, labelText);\n        } else {\n            field = Object.assign({}, this.fields[this._getFieldName()]);\n            field.string = labelText;\n            field.type = this._getFieldType();\n        }\n        if (!noRecords) {\n            field.records = this._getListItems();\n        }\n        this._setActiveProperties(field);\n        return field;\n    },\n    /**\n     * Returns the format object of a field containing\n     * the position, labelWidth and bootstrap col class\n     *\n     * @private\n     * @returns {Object}\n     */\n    _getFieldFormat: function () {\n        let requiredMark, optionalMark;\n        const mark = this.$target[0].querySelector('.s_website_form_mark');\n        if (mark) {\n            requiredMark = this._isFieldRequired();\n            optionalMark = !requiredMark;\n        }\n        const multipleInput = this._getMultipleInputs();\n        const format = {\n            labelPosition: this._getLabelPosition(),\n            labelWidth: this.$target[0].querySelector('.s_website_form_label').style.width,\n            multiPosition: multipleInput && multipleInput.dataset.display || 'horizontal',\n            col: [...this.$target[0].classList].filter(el => el.match(/^col-/g)).join(' '),\n            requiredMark: requiredMark,\n            optionalMark: optionalMark,\n            mark: mark && mark.textContent,\n        };\n        return format;\n    },\n    /**\n     * Returns the name of the field\n     *\n     * @private\n     * @param {HTMLElement} fieldEl\n     * @returns {string}\n     */\n    _getFieldName: function (fieldEl = this.$target[0]) {\n        const multipleName = fieldEl.querySelector('.s_website_form_multiple');\n        return multipleName ? multipleName.dataset.name : fieldEl.querySelector('.s_website_form_input').name;\n    },\n    /**\n     * Returns the type of the  field, can be used for both custom and existing fields\n     *\n     * @private\n     * @returns {string}\n     */\n    _getFieldType: function () {\n        return this.$target[0].dataset.type;\n    },\n    /**\n     * @private\n     * @returns {string}\n     */\n    _getLabelPosition: function () {\n        const label = this.$target[0].querySelector('.s_website_form_label');\n        if (this.$target[0].querySelector('.row:not(.s_website_form_multiple)')) {\n            return label.classList.contains('text-end') ? 'right' : 'left';\n        } else {\n            return label.classList.contains('d-none') ? 'none' : 'top';\n        }\n    },\n    /**\n     * Returns the multiple checkbox/radio element if it exist else null\n     *\n     * @private\n     * @returns {HTMLElement}\n     */\n    _getMultipleInputs: function () {\n        return this.$target[0].querySelector('.s_website_form_multiple');\n    },\n    /**\n     * Returns true if the field is a custom field, false if it is an existing field\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isFieldCustom: function () {\n        return !!this.$target[0].classList.contains('s_website_form_custom');\n    },\n    /**\n     * Returns true if the field is required by the model or by the user.\n     *\n     * @private\n     * @returns {boolean}\n     */\n    _isFieldRequired: function () {\n        const classList = this.$target[0].classList;\n        return classList.contains('s_website_form_required') || classList.contains('s_website_form_model_required');\n    },\n    /**\n     * Set the active field properties on the field Object\n     *\n     * @param {Object} field Field to complete with the active field info\n     */\n    _setActiveProperties(field) {\n        const classList = this.$target[0].classList;\n        const textarea = this.$target[0].querySelector('textarea');\n        const input = this.$target[0].querySelector('input[type=\"text\"], input[type=\"email\"], input[type=\"number\"], input[type=\"tel\"], input[type=\"url\"], textarea');\n        const fileInputEl = this.$target[0].querySelector(\"input[type=file]\");\n        const description = this.$target[0].querySelector('.s_website_form_field_description');\n        field.placeholder = input && input.placeholder;\n        if (input) {\n            // textarea value has no attribute,  date/datetime timestamp property is formated\n            field.value = input.getAttribute('value') || input.value;\n        } else if (field.type === 'boolean') {\n            field.value = !!this.$target[0].querySelector('input[type=\"checkbox\"][checked]');\n        } else if (fileInputEl) {\n            field.maxFilesNumber = fileInputEl.dataset.maxFilesNumber;\n            field.maxFileSize = fileInputEl.dataset.maxFileSize;\n        }\n        // property value is needed for date/datetime (formated date).\n        field.propertyValue = input && input.value;\n        field.description = description && description.outerHTML;\n        field.rows = textarea && textarea.rows;\n        field.required = classList.contains('s_website_form_required');\n        field.modelRequired = classList.contains('s_website_form_model_required');\n        field.hidden = classList.contains('s_website_form_field_hidden');\n        field.formatInfo = this._getFieldFormat();\n    },\n});\n\noptions.registry.WebsiteFormEditor = FormEditor.extend({\n    events: Object.assign({}, options.Class.prototype.events || {}, {\n        'click .toggle-edit-message': '_onToggleEndMessageClick',\n    }),\n\n    /**\n     * @override\n     */\n    init() {\n        this._super(...arguments);\n        this.notification = this.bindService(\"notification\");\n        this.dialog = this.bindService(\"dialog\");\n    },\n    /**\n     * @override\n     */\n    willStart: async function () {\n        const _super = this._super.bind(this);\n\n        // Hide change form parameters option for forms\n        // e.g. User should not be enable to change existing job application form\n        // to opportunity form in 'Apply job' page.\n        this.modelCantChange = this.$target.attr('hide-change-model') !== undefined;\n        this.models = await this._fetchModels();\n\n        const targetModelName = this.$target[0].dataset.model_name || 'mail.mail';\n        this.activeForm = this.models.find(m => m.model === targetModelName);\n        currentActionName = this.activeForm && this.activeForm.website_form_label\n\n        this._makeSelectAction();\n        return _super(...arguments);\n    },\n\n    _fetchModels() {\n        // Get list of website_form compatible models.\n        return this.orm.call(\"ir.model\", \"get_compatible_form_models\");\n    },\n\n    _makeSelectAction() {\n        if (!this.modelCantChange) {\n            // Create the Form Action select\n            this.selectActionEl = document.createElement('we-select');\n            this.selectActionEl.setAttribute('string', 'Action');\n            this.selectActionEl.dataset.noPreview = 'true';\n            this.models.forEach(el => {\n                const option = document.createElement('we-button');\n                option.textContent = el.website_form_label;\n                option.dataset.selectAction = el.id;\n                this.selectActionEl.append(option);\n            });\n            return this.selectActionEl;\n        }\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        const proms = [this._super(...arguments)];\n        // Disable text edition\n        this.$target.attr('contentEditable', false);\n        // Identify editable elements of the form: buttons, description,\n        // recaptcha and columns which are not fields.\n        const formEditableSelector = [\n            \".s_website_form_send\",\n            \".s_website_form_field_description\",\n            \".s_website_form_recaptcha\",\n            \".row > div:not(.s_website_form_field, .s_website_form_submit, .s_website_form_field *, .s_website_form_submit *)\",\n        ].map(selector => `:scope ${selector}`).join(\", \");\n        for (const formEditableEl of this.$target[0].querySelectorAll(formEditableSelector)) {\n            formEditableEl.contentEditable = \"true\";\n        }\n        // Get potential message\n        this.$message = this.$target.parent().find('.s_website_form_end_message');\n        this.showEndMessage = false;\n        // If the form has no model it means a new snippet has been dropped.\n        // Apply the default model selected in willStart on it.\n        if (!this.$target[0].dataset.model_name) {\n            proms.push(this._applyFormModel());\n        }\n        // Get the email_to value from the data-for attribute if it exists. We\n        // use it if there is no value on the email_to input.\n        const formId = this.$target[0].id;\n        const dataForValues = wUtils.getParsedDataFor(formId, this.$target[0].ownerDocument);\n        if (dataForValues) {\n            this.dataForEmailTo = dataForValues['email_to'];\n        }\n        this.defaultEmailToValue = \"info@yourcompany.example.com\";\n        return Promise.all(proms);\n    },\n    /**\n     * @override\n     */\n    cleanForSave: function () {\n        const model = this.$target[0].dataset.model_name;\n        // because apparently this can be called on the wrong widget and\n        // we may not have a model, or fields...\n        if (model) {\n            // we may be re-whitelisting already whitelisted fields. Doesn't\n            // really matter.\n            const fields = [...this.$target[0].querySelectorAll('.s_website_form_field:not(.s_website_form_custom) .s_website_form_input')].map(el => el.name);\n            if (fields.length) {\n                // ideally we'd only do this if saving the form\n                // succeeds... but no idea how to do that\n                this.orm.call(\"ir.model.fields\", \"formbuilder_whitelist\", [model, unique(fields)]);\n            }\n        }\n        if (this.$message.length) {\n            this.$target.removeClass('d-none');\n            this.$message.addClass(\"d-none\");\n        }\n    },\n    /**\n     * @override\n     */\n    updateUI: async function () {\n        // If we want to rerender the xml we need to avoid the updateUI\n        // as they are asynchronous and the ui might try to update while\n        // we are building the UserValueWidgets.\n        if (this.rerender) {\n            this.rerender = false;\n            await this._rerenderXML();\n            return;\n        }\n        await this._super.apply(this, arguments);\n        // End Message UI\n        this.updateUIEndMessage();\n    },\n    /**\n     * @see this.updateUI\n     */\n    updateUIEndMessage: function () {\n        this.$target.toggleClass(\"d-none\", this.showEndMessage);\n        this.$message.toggleClass(\"d-none\", !this.showEndMessage);\n        this.$el.find(\".toggle-edit-message\").toggleClass('text-primary', this.showEndMessage);\n    },\n    /**\n     * @override\n     */\n    notify: function (name, data) {\n        this._super(...arguments);\n        if (name === 'field_mark') {\n            this._setLabelsMark();\n        } else if (name === 'add_field') {\n            const field = this._getCustomField('char', 'Custom Text');\n            field.formatInfo = data.formatInfo;\n            field.formatInfo.requiredMark = this._isRequiredMark();\n            field.formatInfo.optionalMark = this._isOptionalMark();\n            field.formatInfo.mark = this._getMark();\n            const fieldEl = this._renderField(field);\n            data.$target.after(fieldEl);\n            this.trigger_up('activate_snippet', {\n                $snippet: $(fieldEl),\n            });\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Select the value of a field (hidden) that will be used on the model as a preset.\n     * ie: The Job you apply for if the form is on that job's page.\n     */\n    addActionField: function (previewMode, value, params) {\n        // Remove old property fields.\n        authorizedFieldsCache.get(this.$target[0], this.orm).then((fields) => {\n            for (const [fieldName, field] of Object.entries(fields)) {\n                if (field._property) {\n                    for (const inputEl of this.$target[0].querySelectorAll(`[name=\"${fieldName}\"]`)) {\n                        inputEl.closest(\".s_website_form_field\").remove();\n                    }\n                }\n            }\n        });\n        const fieldName = params.fieldName;\n        if (params.isSelect === 'true') {\n            value = parseInt(value);\n        }\n        this._addHiddenField(value, fieldName);\n        // Existing field editors need to be rebuilt with the correct list of\n        // available fields.\n        this.trigger_up('activate_snippet', {\n            $snippet: this.$target,\n        });\n    },\n    /**\n     * Prompts the user to save changes before being redirected\n     * towards an action specified in value.\n     *\n     * @see this.selectClass for parameters\n     */\n    promptSaveRedirect: function (name, value, widgetValue) {\n        return new Promise((resolve, reject) => {\n            const message = _t(\"Would you like to save before being redirected? Unsaved changes will be discarded.\");\n            this.dialog.add(ConfirmationDialog, {\n                body: message,\n                confirmLabel: _t(\"Save\"),\n                confirm: () => {\n                   this.trigger_up('request_save', {\n                        reload: false,\n                        onSuccess: () => {\n                            this._redirectToAction(value);\n                        },\n                        onFailure: () => {\n                            this.notification.add(_t(\"Something went wrong.\"), {\n                                type: 'danger',\n                                sticky: true,\n                            });\n                            reject();\n                        },\n                    });\n                    resolve();\n                },\n                cancel: () => resolve(),\n            });\n        });\n    },\n    /**\n     * Changes the onSuccess event.\n     */\n    onSuccess: function (previewMode, value, params) {\n        this.$target[0].dataset.successMode = value;\n        if (value === 'message') {\n            if (!this.$message.length) {\n                this.$message = $(renderToElement('website.s_website_form_end_message'));\n            }\n            this.$target.after(this.$message);\n        } else {\n            this.showEndMessage = false;\n            this.$message.remove();\n        }\n    },\n    /**\n     * Select the model to create with the form.\n     */\n    selectAction: async function (previewMode, value, params) {\n        if (this.modelCantChange) {\n            return;\n        }\n        await this._applyFormModel(parseInt(value));\n        this.rerender = true;\n    },\n    /**\n     * @override\n     */\n    selectClass: function (previewMode, value, params) {\n        this._super(...arguments);\n        if (params.name === 'field_mark_select') {\n            this._setLabelsMark();\n        }\n    },\n    /**\n     * Set the mark string on the form\n     */\n    setMark: function (previewMode, value, params) {\n        this.$target[0].dataset.mark = value.trim();\n        this._setLabelsMark();\n    },\n    /**\n     * Toggle the recaptcha legal terms\n     */\n    toggleRecaptchaLegal: function (previewMode, value, params) {\n        const recaptchaLegalEl = this.$target[0].querySelector('.s_website_form_recaptcha');\n        if (recaptchaLegalEl) {\n            recaptchaLegalEl.remove();\n        } else {\n            const labelWidth = this.$target[0].querySelector('.s_website_form_label').style.width;\n            const legal = renderToElement(\"website.s_website_form_recaptcha_legal\", {\n                labelWidth: labelWidth,\n            });\n            legal.setAttribute('contentEditable', true);\n            this.$target.find('.s_website_form_submit').before(legal);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'selectAction':\n                return this.activeForm.id;\n            case 'addActionField': {\n                const value = this.$target.find(`.s_website_form_dnone input[name=\"${params.fieldName}\"]`).val();\n                if (params.fieldName === 'email_to') {\n                    // For email_to, we try to find a value in this order:\n                    // 1. The current value of the input\n                    // 2. The data-for value if it exists\n                    // 3. The default value (`defaultEmailToValue`)\n                    if (value && value !== this.defaultEmailToValue) {\n                        return value;\n                    }\n                    return this.dataForEmailTo || this.defaultEmailToValue;\n                }\n                if (value) {\n                    return value;\n                } else {\n                    return params.isSelect ? '0' : '';\n                }\n            }\n            case 'onSuccess':\n                return this.$target[0].dataset.successMode;\n            case 'setMark':\n                return this._getMark();\n            case 'toggleRecaptchaLegal':\n                return !this.$target[0].querySelector('.s_website_form_recaptcha') || '';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _renderCustomXML: function (uiFragment) {\n        if (this.modelCantChange) {\n            return;\n        }\n        // Add Action select\n        const firstOption = uiFragment.childNodes[0];\n        uiFragment.insertBefore(this.selectActionEl.cloneNode(true), firstOption);\n\n        // Add Action related options\n        const formKey = this.activeForm.website_form_key;\n        const formInfo = FormEditorRegistry.get(formKey, null);\n        if (!formInfo || !formInfo.fields) {\n            return;\n        }\n        allFormsInfo.set(this.$target[0], formInfo);\n        const proms = formInfo.fields.map(field => this._fetchFieldRecords(field));\n        return Promise.all(proms).then(() => {\n            formInfo.fields.forEach(field => {\n                let option;\n                switch (field.type) {\n                    case 'many2one':\n                        option = this._buildSelect(field);\n                        break;\n                    case 'char':\n                        option = this._buildInput(field);\n                        break;\n                }\n                if (field.required) {\n                    // Try to retrieve hidden value in form, else,\n                    // get default value or for many2one fields the first option.\n                    const currentValue = this.$target.find(`.s_website_form_dnone input[name=\"${field.name}\"]`).val();\n                    const defaultValue = field.defaultValue || field.records[0].id;\n                    // TODO this code is not rightfully placed (even maybe\n                    // from the original form feature in older versions). It\n                    // changes the $target while this method is only about\n                    // declaring the option UI. This for example forces the\n                    // 'email_to' value to a dummy value on contact us form just\n                    // by clicking on it.\n                    this._addHiddenField(currentValue || defaultValue, field.name);\n                }\n                uiFragment.insertBefore(option, firstOption);\n            });\n        });\n    },\n    /**\n     * Add a hidden field to the form\n     *\n     * @private\n     * @param {string} value\n     * @param {string} fieldName\n     */\n    _addHiddenField: function (value, fieldName) {\n        this.$target.find(`.s_website_form_dnone:has(input[name=\"${fieldName}\"])`).remove();\n        // For the email_to field, we keep the field even if it has no value so\n        // that the email is sent to data-for value or to the default email.\n        if (fieldName === 'email_to' && !value && !this.dataForEmailTo) {\n            value = this.defaultEmailToValue;\n        }\n        if (value || fieldName === 'email_to') {\n            const hiddenField = renderToElement('website.form_field_hidden', {\n                field: {\n                    name: fieldName,\n                    value: value,\n                    dnone: true,\n                    formatInfo: {},\n                },\n            });\n            this.$target.find('.s_website_form_submit').before(hiddenField);\n        }\n    },\n    /**\n     * Returns a we-input element from the field\n     *\n     * @private\n     * @param {Object} field\n     * @returns {HTMLElement}\n     */\n    _buildInput: function (field) {\n        const inputEl = document.createElement('we-input');\n        inputEl.dataset.noPreview = 'true';\n        inputEl.dataset.fieldName = field.name;\n        inputEl.dataset.addActionField = '';\n        inputEl.setAttribute('string', field.string);\n        inputEl.classList.add('o_we_large');\n        return inputEl;\n    },\n    /**\n     * Returns a we-select element with field's records as it's options\n     *\n     * @private\n     * @param {Object} field\n     * @return {HTMLElement}\n     */\n    _buildSelect: function (field) {\n        const selectEl = document.createElement('we-select');\n        selectEl.dataset.noPreview = 'true';\n        selectEl.dataset.fieldName = field.name;\n        selectEl.dataset.isSelect = 'true';\n        selectEl.setAttribute('string', field.string);\n        if (!field.required) {\n            const noneButton = document.createElement('we-button');\n            noneButton.textContent = 'None';\n            noneButton.dataset.addActionField = 0;\n            selectEl.append(noneButton);\n        }\n        field.records.forEach(el => {\n            const button = document.createElement('we-button');\n            button.textContent = el.display_name;\n            button.dataset.addActionField = el.id;\n            selectEl.append(button);\n        });\n        if (field.createAction) {\n            return this._addCreateButton(selectEl, field.createAction);\n        }\n        return selectEl;\n    },\n    /**\n     * Wraps an HTML element in a we-row element, and adds a\n     * we-button linking to the given action.\n     *\n     * @private\n     * @param {HTMLElement} element\n     * @param {String} action\n     * @returns {HTMLElement}\n     */\n    _addCreateButton: function (element, action) {\n        const linkButtonEl = document.createElement('we-button');\n        linkButtonEl.title = _t(\"Create new\");\n        linkButtonEl.dataset.noPreview = 'true';\n        linkButtonEl.dataset.promptSaveRedirect = action;\n        linkButtonEl.classList.add('fa', 'fa-fw', 'fa-plus');\n        const projectRowEl = document.createElement('we-row');\n        projectRowEl.append(element);\n        projectRowEl.append(linkButtonEl);\n        return projectRowEl;\n    },\n    /**\n     * Apply the model on the form changing it's fields\n     *\n     * @private\n     * @param {Integer} modelId\n     */\n    _applyFormModel: async function (modelId) {\n        let oldFormInfo;\n        if (modelId) {\n            const oldFormKey = this.activeForm.website_form_key;\n            if (oldFormKey) {\n                oldFormInfo = FormEditorRegistry.get(oldFormKey, null);\n            }\n            this.$target.find('.s_website_form_field').remove();\n            this.activeForm = this.models.find(model => model.id === modelId);\n            currentActionName = this.activeForm.website_form_label;\n        }\n        const formKey = this.activeForm.website_form_key;\n        const formInfo = FormEditorRegistry.get(formKey, null);\n        // Success page\n        if (!this.$target[0].dataset.successMode) {\n            this.$target[0].dataset.successMode = 'redirect';\n        }\n        if (this.$target[0].dataset.successMode === 'redirect') {\n            const currentSuccessPage = this.$target[0].dataset.successPage;\n            if (formInfo && formInfo.successPage) {\n                this.$target[0].dataset.successPage = formInfo.successPage;\n            } else if (!oldFormInfo || (oldFormInfo !== formInfo && oldFormInfo.successPage && currentSuccessPage === oldFormInfo.successPage)) {\n                this.$target[0].dataset.successPage = '/contactus-thank-you';\n            }\n        }\n        // Model name\n        this.$target[0].dataset.model_name = this.activeForm.model;\n        // Load template\n        if (formInfo) {\n            const formatInfo = this._getDefaultFormat();\n            await formInfo.formFields.forEach(async field => {\n                field.formatInfo = formatInfo;\n                await this._fetchFieldRecords(field);\n                this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(this._renderField(field));\n            });\n        }\n    },\n    /**\n     * Set the correct mark on all fields.\n     *\n     * @private\n     */\n    _setLabelsMark: function () {\n        this.$target[0].querySelectorAll('.s_website_form_mark').forEach(el => el.remove());\n        const mark = this._getMark();\n        if (!mark) {\n            return;\n        }\n        let fieldsToMark = [];\n        const requiredSelector = '.s_website_form_model_required, .s_website_form_required';\n        const fields = Array.from(this.$target[0].querySelectorAll('.s_website_form_field'));\n        if (this._isRequiredMark()) {\n            fieldsToMark = fields.filter(el => el.matches(requiredSelector));\n        } else if (this._isOptionalMark()) {\n            fieldsToMark = fields.filter(el => !el.matches(requiredSelector));\n        }\n        fieldsToMark.forEach(field => {\n            let span = document.createElement('span');\n            span.classList.add('s_website_form_mark');\n            span.textContent = ` ${mark}`;\n            field.querySelector('.s_website_form_label').appendChild(span);\n        });\n    },\n    /**\n     * Redirects the user to the page of a specified action.\n     *\n     * @private\n     * @param {string} action\n     */\n    _redirectToAction: function (action) {\n        redirect(`/odoo/action-${encodeURIComponent(action)}`);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onToggleEndMessageClick: function () {\n        this.showEndMessage = !this.showEndMessage;\n        this.updateUIEndMessage();\n        this.trigger_up('activate_snippet', {\n            $snippet: this.showEndMessage ? this.$message : this.$target,\n        });\n    },\n});\n\noptions.registry.WebsiteFieldEditor = FieldEditor.extend({\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this.rerender = true;\n        this._getVisibilityConditionCachedRecords = memoize(\n            (model, domain, fields, kwargs = {}) => {\n                return this.orm.searchRead(model, domain, fields, {\n                    ...kwargs,\n                    limit: 1000, // Safeguard to not crash DBs\n                });\n            },\n        );\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        const _super = this._super.bind(this);\n        // Build the custom select\n        const select = this._getSelect();\n        if (select) {\n            const field = this._getActiveField();\n            await this._replaceField(field);\n        }\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     */\n    updateUI: async function () {\n        // See Form updateUI\n        if (this.rerender) {\n            this.rerender = false;\n            await this._rerenderXML();\n            return;\n        }\n        await this._super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    onFocus: function () {\n        // Other fields type might have change to an existing type.\n        // We need to reload the existing type list.\n        this.rerender = true;\n    },\n    /**\n     * Rerenders the clone to avoid id duplicates.\n     *\n     * @override\n     */\n    onClone() {\n        const field = this._getActiveField();\n        delete field.id;\n        const fieldEl = this._renderField(field);\n        this._replaceFieldElement(fieldEl);\n    },\n    /**\n     * Removes the visibility conditions concerned by the deleted field\n     *\n     * @override\n     */\n    onRemove() {\n        const fieldName = this.$target[0].querySelector('.s_website_form_input').name;\n        const isMultipleField = this.formEl.querySelectorAll(`.s_website_form_input[name=\"${CSS.escape(fieldName)}\"]`).length > 1;\n        if (isMultipleField) {\n            return;\n        }\n        const dependentFieldContainerEl = this.formEl.querySelectorAll(`[data-visibility-dependency=\"${CSS.escape(fieldName)}\"]`);\n        for (const fieldContainerEl of dependentFieldContainerEl) {\n            this._deleteConditionalVisibility(fieldContainerEl);\n        }\n    },\n\n    //----------------------------------------------------------------------\n    // Options\n    //----------------------------------------------------------------------\n\n    /**\n     * Add/remove a description to the field input\n     */\n    toggleDescription: async function (previewMode, value, params) {\n        const field = this._getActiveField();\n        field.description = !!value; // Will be changed to default description in qweb\n        await this._replaceField(field);\n    },\n    /**\n     * Replace the current field with the custom field selected.\n     */\n    customField: async function (previewMode, value, params) {\n        // Both custom Field and existingField are called when selecting an option\n        // value is '' for the method that should not be called.\n        if (!value) {\n            return;\n        }\n        const oldLabelText = this.$target[0].querySelector('.s_website_form_label_content').textContent;\n        const field = this._getCustomField(value, oldLabelText);\n        this._setActiveProperties(field);\n        await this._replaceField(field);\n        this.rerender = true;\n    },\n    /**\n     * Replace the current field with the existing field selected.\n     */\n    existingField: async function (previewMode, value, params) {\n        // see customField\n        if (!value) {\n            return;\n        }\n        const field = Object.assign({}, this.fields[value]);\n        this._setActiveProperties(field);\n        await this._replaceField(field);\n        this.rerender = true;\n    },\n    /**\n     * Set the the selction type of existing fields (radio or dropdown).\n     *\n     * @see this.selectClass for parameters\n     */\n    async existingFieldSelectType(previewMode, value, params) {\n        const field = this._getActiveField();\n        field.type = value;\n        await this._replaceField(field);\n    },\n    /**\n     * Set the name of the field on the label\n     */\n    setLabelText: function (previewMode, value, params) {\n        this.$target.find('.s_website_form_label_content').text(value);\n        if (this._isFieldCustom()) {\n            value = this._getQuotesEncodedName(value);\n            const multiple = this.$target[0].querySelector('.s_website_form_multiple');\n            if (multiple) {\n                multiple.dataset.name = value;\n            }\n            const inputEls = this.$target[0].querySelectorAll('.s_website_form_input');\n            const previousInputName = inputEls[0].name;\n            inputEls.forEach(el => el.name = value);\n\n            // Synchronize the fields whose visibility depends on this field\n            const dependentEls = this.formEl.querySelectorAll(`.s_website_form_field[data-visibility-dependency=\"${CSS.escape(previousInputName)}\"]`);\n            for (const dependentEl of dependentEls) {\n                if (!previewMode && this._findCircular(this.$target[0], dependentEl)) {\n                    // For all the fields whose visibility depends on this\n                    // field, check if the new name creates a circular\n                    // dependency and remove the problematic conditional\n                    // visibility if it is the case. E.g. a field (A) depends on\n                    // another (B) and the user renames \"B\" by \"A\".\n                    this._deleteConditionalVisibility(dependentEl);\n                } else {\n                    dependentEl.dataset.visibilityDependency = value;\n                }\n            }\n\n            if (!previewMode) {\n                // As the field label changed, the list of available visibility\n                // dependencies needs to be updated in order to not propose a\n                // field that would create a circular dependency.\n                this.rerender = true;\n            }\n        }\n    },\n    /**\n     * Replace the field with the same field having the label in a different position.\n     */\n    selectLabelPosition: async function (previewMode, value, params) {\n        const field = this._getActiveField();\n        field.formatInfo.labelPosition = value;\n        await this._replaceField(field);\n    },\n    selectType: async function (previewMode, value, params) {\n        const field = this._getActiveField();\n        field.type = value;\n        await this._replaceField(field);\n    },\n    /**\n     * Select the textarea default value\n     */\n    selectTextareaValue: function (previewMode, value, params) {\n        this.$target[0].textContent = value;\n        this.$target[0].value = value;\n    },\n    /**\n     * Select the date as value property and convert it to the right format\n     */\n    selectValueProperty: function (previewMode, value, params) {\n        const [target] = this.$target;\n        const field = target.closest(\".s_website_form_date, .s_website_form_datetime\");\n        const format = field.matches(\".s_website_form_date\") ? formatDate : formatDateTime;\n        target.value = value ? format(luxon.DateTime.fromSeconds(parseInt(value))) : \"\";\n    },\n    /**\n     * Select the display of the multicheckbox field (vertical & horizontal)\n     */\n    multiCheckboxDisplay: function (previewMode, value, params) {\n        const target = this._getMultipleInputs();\n        target.querySelectorAll('.checkbox, .radio').forEach(el => {\n            if (value === 'horizontal') {\n                el.classList.add('col-lg-4', 'col-md-6');\n            } else {\n                el.classList.remove('col-lg-4', 'col-md-6');\n            }\n        });\n        target.dataset.display = value;\n    },\n    /**\n     * Set the field as required or not\n     */\n    toggleRequired: function (previewMode, value, params) {\n        const isRequired = this.$target[0].classList.contains(params.activeValue);\n        this.$target[0].classList.toggle(params.activeValue, !isRequired);\n        this.$target[0].querySelectorAll('input, select, textarea').forEach(el => el.toggleAttribute('required', !isRequired));\n        this.trigger_up('option_update', {\n            optionName: 'WebsiteFormEditor',\n            name: 'field_mark',\n        });\n    },\n    /**\n     * Apply the we-list on the target and rebuild the input(s)\n     */\n    renderListItems: async function (previewMode, value, params) {\n        let valueList = JSON.parse(value);\n        if (this._getSelect()) {\n            // Default entry only for fields rendered as select.\n            // Remove previous default.\n            valueList = valueList.filter(value => value.id !== \"\" || value.display_name !== \"\");\n            // Add default in first position if no default value is set.\n            const hasDefault = valueList.some(value => value.selected);\n            if (valueList.length && !hasDefault) {\n                valueList.unshift({\n                    id: \"\",\n                    display_name: \"\",\n                    selected: true,\n                });\n            }\n        }\n\n        // Synchronize the possible values with the fields whose visibility\n        // depends on the current field\n        const newValuesText = valueList.map(value => value.name);\n        const inputEls = this.$target[0].querySelectorAll('.s_website_form_input, option');\n        const inputName = this.$target[0].querySelector('.s_website_form_input').name;\n        for (let i = 0; i < inputEls.length; i++) {\n            const input = inputEls[i];\n            if (newValuesText[i] && input.value && !newValuesText.includes(input.value)) {\n                for (const dependentEl of this.formEl.querySelectorAll(\n                        `[data-visibility-condition=\"${CSS.escape(input.value)}\"][data-visibility-dependency=\"${CSS.escape(inputName)}\"]`)) {\n                    dependentEl.dataset.visibilityCondition = newValuesText[i];\n                }\n                break;\n            }\n        }\n\n        const field = this._getActiveField(true);\n        field.records = valueList;\n        await this._replaceField(field);\n    },\n    /**\n     * Sets the visibility of the field.\n     *\n     * @see this.selectClass for parameters\n     */\n    setVisibility(previewMode, widgetValue, params) {\n        if (widgetValue === 'conditional') {\n            const widget = this.findWidget('hidden_condition_opt');\n            const firstValue = widget.getMethodsParams('setVisibilityDependency').possibleValues.find(el => el !== '');\n            if (firstValue) {\n                // Set a default visibility dependency\n                this._setVisibilityDependency(firstValue);\n                return;\n            }\n            this.dialog.add(ConfirmationDialog, {\n                body: _t(\"There is no field available for this option.\"),\n            });\n        }\n        this._deleteConditionalVisibility(this.$target[0]);\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setVisibilityDependency(previewMode, widgetValue, params) {\n        this._setVisibilityDependency(widgetValue);\n    },\n    /**\n     * @override\n     */\n    async selectDataAttribute(previewMode, widgetValue, params) {\n        await this._super(...arguments);\n        if (params.attributeName === \"maxFilesNumber\") {\n            const allowMultipleFiles = params.activeValue > 1;\n            this.$target[0].toggleAttribute(\"multiple\", allowMultipleFiles);\n        }\n    },\n\n    //----------------------------------------------------------------------\n    // Private\n    //----------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        switch (methodName) {\n            case 'toggleDescription': {\n                const description = this.$target[0].querySelector('.s_website_form_field_description');\n                return !!description;\n            }\n            case 'customField':\n                return this._isFieldCustom() ? this._getFieldType() : '';\n            case 'existingField':\n                return this._isFieldCustom() ? '' : this._getFieldName();\n            case 'setLabelText':\n                return this.$target.find('.s_website_form_label_content').text();\n            case 'selectLabelPosition':\n                return this._getLabelPosition();\n            case 'selectType':\n            case \"existingFieldSelectType\":\n                return this._getFieldType();\n            case 'selectTextareaValue':\n                return this.$target[0].textContent;\n            case 'selectValueProperty':\n                return this.$target[0].getAttribute('value') || '';\n            case 'multiCheckboxDisplay': {\n                const target = this._getMultipleInputs();\n                return target ? target.dataset.display : '';\n            }\n            case 'toggleRequired':\n                return this.$target[0].classList.contains(params.activeValue) ? params.activeValue : 'false';\n            case 'renderListItems':\n                // TODO In master use a parameter.\n                this.__getListItems_forWidgetState = true;\n                try {\n                    return JSON.stringify(this._getListItems());\n                } finally {\n                    delete this.__getListItems_forWidgetState;\n                }\n            case 'setVisibilityDependency':\n                return this.$target[0].dataset.visibilityDependency || '';\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _computeWidgetVisibility: function (widgetName, params) {\n        const dependencyEl = this._getDependencyEl();\n        switch (widgetName) {\n            case 'hidden_condition_time_comparators_opt':\n                return dependencyEl?.classList.contains(\"datetimepicker-input\");\n            case 'hidden_condition_date_between':\n                return dependencyEl?.closest(\".s_website_form_date\")\n                && ['between', '!between'].includes(this.$target[0].getAttribute('data-visibility-comparator'));\n            case 'hidden_condition_datetime_between':\n                return dependencyEl?.closest(\".s_website_form_datetime\")\n                && ['between', '!between'].includes(this.$target[0].dataset.visibilityComparator);\n            case 'hidden_condition_additional_datetime':\n                return dependencyEl?.closest(\".s_website_form_datetime\")\n                && !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);\n            case 'hidden_condition_additional_date':\n                return dependencyEl && dependencyEl?.closest(\".s_website_form_date\")\n                && !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);\n            case 'hidden_condition_additional_text':\n                if (!this.$target[0].classList.contains('s_website_form_field_hidden_if') ||\n                (dependencyEl && (['checkbox', 'radio'].includes(dependencyEl.type) || dependencyEl.nodeName === 'SELECT'))) {\n                    return false;\n                }\n                if (!dependencyEl) {\n                    return true;\n                }\n                if (dependencyEl?.classList.contains(\"datetimepicker-input\")) {\n                    return false;\n                }\n                return (['text', 'email', 'tel', 'url', 'search', 'password', 'number'].includes(dependencyEl.type)\n                    || dependencyEl.nodeName === 'TEXTAREA') && !['set', '!set'].includes(this.$target[0].dataset.visibilityComparator);\n            case 'hidden_condition_no_text_opt':\n                return dependencyEl && (dependencyEl.type === 'checkbox' || dependencyEl.type === 'radio' || dependencyEl.nodeName === 'SELECT');\n            case 'hidden_condition_num_opt':\n                return dependencyEl && dependencyEl.type === 'number';\n            case 'hidden_condition_text_opt':\n                if (!this.$target[0].classList.contains('s_website_form_field_hidden_if') ||\n                (dependencyEl?.classList.contains(\"datetimepicker-input\"))) {\n                    return false;\n                }\n                return !dependencyEl || (['text', 'email', 'tel', 'url', 'search', 'password'].includes(dependencyEl.type) ||\n                dependencyEl.nodeName === 'TEXTAREA');\n            case 'hidden_condition_date_opt':\n                return dependencyEl?.closest(\".s_website_form_date\");\n            case 'hidden_condition_datetime_opt':\n                return dependencyEl?.closest(\".s_website_form_datetime\");\n            case 'hidden_condition_file_opt':\n                return dependencyEl && dependencyEl.type === 'file';\n            case \"hidden_condition_record_opt\":\n                return dependencyEl?.closest(\".s_website_form_field\")?.dataset.type === \"record\";\n            case 'hidden_condition_opt':\n                return this.$target[0].classList.contains('s_website_form_field_hidden_if');\n            case 'char_input_type_opt':\n                return !this.$target[0].classList.contains('s_website_form_custom') &&\n                    ['char', 'email', 'tel', 'url'].includes(this.$target[0].dataset.type) &&\n                    !this.$target[0].classList.contains('s_website_form_model_required');\n            case \"existing_field_select_type_opt\":\n                return !this._isFieldCustom() &&\n                        [\"selection\", \"many2one\"].includes(this.$target[0].dataset.type);\n            case 'multi_check_display_opt':\n                return !!this._getMultipleInputs();\n            case 'required_opt':\n            case 'hidden_opt':\n            case 'type_opt':\n                return !this.$target[0].classList.contains('s_website_form_model_required');\n            case \"max_files_number_opt\": {\n                // Do not display the option if only one file is supposed to be\n                // uploaded in the field.\n                const fieldEl = this.$target[0].closest(\".s_website_form_field\");\n                return fieldEl.classList.contains(\"s_website_form_custom\") ||\n                    [\"one2many\", \"many2many\"].includes(fieldEl.dataset.type);\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * Deletes all attributes related to conditional visibility.\n     *\n     * @param {HTMLElement} fieldEl\n     */\n     _deleteConditionalVisibility(fieldEl) {\n        for (const name of this.VISIBILITY_DATASET) {\n            delete fieldEl.dataset[name];\n        }\n        fieldEl.classList.remove('s_website_form_field_hidden_if', 'd-none');\n    },\n    /**\n     * @param {HTMLElement} [fieldEl]\n     * @returns {HTMLElement} The visibility dependency of the field\n     */\n    _getDependencyEl(fieldEl = this.$target[0]) {\n        const dependencyName = fieldEl.dataset.visibilityDependency;\n        return this.formEl.querySelector(`.s_website_form_input[name=\"${CSS.escape(dependencyName)}\"]`);\n    },\n    /**\n     * @param {HTMLElement} dependentFieldEl\n     * @param {HTMLElement} targetFieldEl\n     * @returns {boolean} \"true\" if adding \"dependentFieldEl\" or any other field\n     * with the same label in the conditional visibility of \"targetFieldEl\"\n     * would create a circular dependency involving \"targetFieldEl\".\n     */\n    _findCircular(dependentFieldEl, targetFieldEl = this.$target[0]) {\n        // Keep a register of the already visited fields to not enter an\n        // infinite check loop.\n        const visitedFields = new Set();\n        const recursiveFindCircular = (dependentFieldEl, targetFieldEl) => {\n            const dependentFieldName = this._getFieldName(dependentFieldEl);\n            // Get all the fields that have the same label as the dependent\n            // field.\n            let dependentFieldEls = Array.from(this.formEl\n                .querySelectorAll(`.s_website_form_input[name=\"${CSS.escape(dependentFieldName)}\"]`))\n                .map((el) => el.closest(\".s_website_form_field\"));\n            // Remove the duplicated fields. This could happen if the field has\n            // multiple inputs (\"Multiple Checkboxes\" for example.)\n            dependentFieldEls = new Set(dependentFieldEls);\n            const fieldName = this._getFieldName(targetFieldEl);\n            for (const dependentFieldEl of dependentFieldEls) {\n                // Only check for circular dependencies on fields that do not\n                // already have been checked.\n                if (!(visitedFields.has(dependentFieldEl))) {\n                    // Add the dependentFieldEl in the set of checked field.\n                    visitedFields.add(dependentFieldEl);\n                    if (dependentFieldEl.dataset.visibilityDependency === fieldName) {\n                        return true;\n                    }\n                    const dependencyInputEl = this._getDependencyEl(dependentFieldEl);\n                    if (dependencyInputEl && recursiveFindCircular(dependencyInputEl.closest(\".s_website_form_field\"), targetFieldEl)) {\n                        return true;\n                    }\n                }\n            }\n            return false;\n        };\n        return recursiveFindCircular(dependentFieldEl, targetFieldEl);\n    },\n    /**\n     * @override\n     */\n    _renderCustomXML: async function (uiFragment) {\n        // Get the authorized existing fields for the form model\n        // Do it on each render because of custom property fields which can\n        // change depending on the project selected.\n        this.existingFields = await authorizedFieldsCache.get(this.formEl, this.orm).then((fields) => {\n            this.fields = {};\n            for (const [fieldName, field] of Object.entries(fields)) {\n                field.name = fieldName;\n                const fieldDomain = _getDomain(this.formEl, field.name, field.type, field.relation);\n                field.domain = fieldDomain || field.domain || [];\n                this.fields[fieldName] = field;\n            }\n            // Create the buttons for the type we-select\n            return Object.keys(fields).map(key => {\n                const field = fields[key];\n                const button = document.createElement('we-button');\n                button.textContent = field.string;\n                button.dataset.existingField = field.name;\n                return button;\n            }).sort((a, b) => a.textContent.localeCompare(b.textContent, undefined, { numeric: true, sensitivity: \"base\" }));\n        });\n        // Update available visibility dependencies\n        const selectDependencyEl = uiFragment.querySelector('we-select[data-name=\"hidden_condition_opt\"]');\n        const existingDependencyNames = [];\n        for (const el of this.formEl.querySelectorAll(\n            \".s_website_form_field:not(.s_website_form_dnone), .s_website_form_field[data-type]\",\n        )) {\n            const inputEl = el.querySelector('.s_website_form_input');\n            if (el.querySelector('.s_website_form_label_content') && inputEl && inputEl.name\n                    && inputEl.name !== this.$target[0].querySelector('.s_website_form_input').name\n                    && !existingDependencyNames.includes(inputEl.name) && !this._findCircular(el)) {\n                const button = document.createElement('we-button');\n                button.textContent = el.querySelector('.s_website_form_label_content').textContent;\n                button.dataset.setVisibilityDependency = inputEl.name;\n                selectDependencyEl.append(button);\n                existingDependencyNames.push(inputEl.name);\n            }\n        }\n\n        const comparator = this.$target[0].dataset.visibilityComparator;\n        const dependencyEl = this._getDependencyEl();\n        if (dependencyEl) {\n            const containerEl = dependencyEl.closest(\".s_website_form_field\");\n            const fieldType = containerEl?.dataset.type;\n            if (\n                [\"radio\", \"checkbox\"].includes(dependencyEl.type) ||\n                dependencyEl.nodeName === \"SELECT\" ||\n                fieldType === \"record\"\n            ) {\n                // Update available visibility options\n                const selectOptName =\n                    fieldType === \"record\"\n                        ? \"hidden_condition_record_opt\"\n                        : \"hidden_condition_no_text_opt\";\n                const selectOptEl = uiFragment.querySelectorAll(\n                    `we-select[data-name=\"${selectOptName}\"]`,\n                )[1];\n                const inputContainerEl = this.$target[0];\n                const dependencyEl = this._getDependencyEl();\n                if (dependencyEl.nodeName === 'SELECT') {\n                    for (const option of dependencyEl.querySelectorAll('option')) {\n                        const button = document.createElement('we-button');\n                        button.textContent = option.textContent || `<${_t(\"no value\")}>`;\n                        button.dataset.selectDataAttribute = option.value;\n                        selectOptEl.append(button);\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition = dependencyEl.querySelector('option').value;\n                    }\n                } else if (fieldType === \"record\") {\n                    const model = containerEl.dataset.model;\n                    const idField = containerEl.dataset.idField || \"id\";\n                    const displayNameField = containerEl.dataset.displayNameField || \"display_name\";\n                    const records = await this._getVisibilityConditionCachedRecords(\n                        model,\n                        [],\n                        [idField, displayNameField],\n                    );\n                    for (const record of records) {\n                        const buttonEl = document.createElement(\"we-button\");\n                        buttonEl.textContent = record[displayNameField];\n                        buttonEl.dataset.selectDataAttribute = record[idField];\n                        selectOptEl.append(buttonEl);\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition = records[0]?.[idField];\n                    }\n                } else { // DependecyEl is a radio or a checkbox\n                    const dependencyContainerEl = dependencyEl.closest('.s_website_form_field');\n                    const inputsInDependencyContainer = dependencyContainerEl.querySelectorAll('.s_website_form_input');\n                    for (const el of inputsInDependencyContainer) {\n                        const button = document.createElement('we-button');\n                        button.textContent = inputsInDependencyContainer.length === 1\n                            ? el.value\n                            : dependencyContainerEl\n                                .querySelector(`label[for=\"${el.id}\"]`)\n                                .textContent;\n                        button.dataset.selectDataAttribute = el.value;\n                        selectOptEl.append(button);\n                    }\n                    if (!inputContainerEl.dataset.visibilityCondition) {\n                        inputContainerEl.dataset.visibilityCondition = inputsInDependencyContainer[0].value;\n                    }\n                }\n                if (!inputContainerEl.dataset.visibilityComparator) {\n                    inputContainerEl.dataset.visibilityComparator = 'selected';\n                }\n                this.rerender = comparator ? this.rerender : true;\n            }\n            if (!comparator) {\n                // Set a default comparator according to the type of dependency\n                if (dependencyEl.dataset.target) {\n                    this.$target[0].dataset.visibilityComparator = 'after';\n                } else if (['text', 'email', 'tel', 'url', 'search', 'password', 'number'].includes(dependencyEl.type)\n                        || dependencyEl.nodeName === 'TEXTAREA') {\n                    this.$target[0].dataset.visibilityComparator = 'equal';\n                } else if (dependencyEl.type === 'file') {\n                    this.$target[0].dataset.visibilityComparator = 'fileSet';\n                }\n            }\n        }\n\n        const selectEl = uiFragment.querySelector('we-select[data-name=\"type_opt\"]');\n        const currentFieldName = this._getFieldName();\n        const fieldsInForm = Array.from(this.formEl.querySelectorAll('.s_website_form_field:not(.s_website_form_custom) .s_website_form_input')).map(el => el.name).filter(el => el !== currentFieldName);\n        const availableFields = this.existingFields.filter(el => !fieldsInForm.includes(el.dataset.existingField));\n        if (availableFields.length) {\n            const title = document.createElement('we-title');\n            title.textContent = 'Existing fields';\n            availableFields.unshift(title);\n            availableFields.forEach(option => selectEl.append(option.cloneNode(true)));\n        }\n\n        const select = this._getSelect();\n        const multipleInputs = this._getMultipleInputs();\n        if (!select && !multipleInputs) {\n            return;\n        }\n\n        const field = Object.assign({}, this.fields[this._getFieldName()]);\n        const type = this._getFieldType();\n\n        const list = document.createElement('we-list');\n        const optionText = select ? 'Option' : type === 'selection' ? 'Radio' : 'Checkbox';\n        list.setAttribute('string', `${optionText} List`);\n        list.dataset.addItemTitle = _t(\"Add new %s\", optionText);\n        list.dataset.renderListItems = '';\n\n        list.dataset.hasDefault = ['one2many', 'many2many'].includes(type) ? 'multiple' : 'unique';\n        const defaults = [...this.$target[0].querySelectorAll('[checked], [selected]')].map(el => {\n            return /^-?[0-9]{1,15}$/.test(el.value) ? parseInt(el.value) : el.value;\n        });\n        list.dataset.defaults = JSON.stringify(defaults);\n\n        if (!this._isFieldCustom()) {\n            await this._fetchFieldRecords(field);\n            list.dataset.availableRecords = JSON.stringify(field.records);\n        }\n        uiFragment.insertBefore(list, uiFragment.querySelector('we-select[string=\"Visibility\"]'));\n    },\n    /**\n     * Replaces the target content with the field provided.\n     *\n     * @private\n     * @param {Object} field\n     * @returns {Promise}\n     */\n    _replaceField: async function (field) {\n        await this._fetchFieldRecords(field);\n        const activeField = this._getActiveField();\n        if (activeField.type !== field.type) {\n            field.value = '';\n        }\n        const fieldEl = this._renderField(field);\n        this._replaceFieldElement(fieldEl);\n    },\n    /**\n     * Replaces the target with provided field.\n     *\n     * @private\n     * @param {HTMLElement} fieldEl\n     */\n    _replaceFieldElement(fieldEl) {\n        const inputEl = this.$target[0].querySelector('input');\n        const dataFillWith = inputEl ? inputEl.dataset.fillWith : undefined;\n        const hasConditionalVisibility = this.$target[0].classList.contains('s_website_form_field_hidden_if');\n        const previousInputEl = this.$target[0].querySelector('.s_website_form_input');\n        const previousName = previousInputEl.name;\n        const previousType = previousInputEl.type;\n        [...this.$target[0].childNodes].forEach(node => node.remove());\n        [...fieldEl.childNodes].forEach(node => this.$target[0].appendChild(node));\n        [...fieldEl.attributes].forEach(el => this.$target[0].removeAttribute(el.nodeName));\n        [...fieldEl.attributes].forEach(el => this.$target[0].setAttribute(el.nodeName, el.nodeValue));\n        if (hasConditionalVisibility) {\n            this.$target[0].classList.add('s_website_form_field_hidden_if', 'd-none');\n        }\n        const dependentFieldEls = this.formEl.querySelectorAll(`.s_website_form_field[data-visibility-dependency=\"${CSS.escape(previousName)}\"]`);\n        const newFormInputEl = this.$target[0].querySelector('.s_website_form_input');\n        const newName = newFormInputEl.name;\n        const newType = newFormInputEl.type;\n        if ((previousName !== newName || previousType !== newType) && dependentFieldEls) {\n            // In order to keep the visibility conditions consistent,\n            // when the name has changed, it means that the type has changed so\n            // all fields whose visibility depends on this field must be updated so that\n            // they no longer have conditional visibility\n            for (const fieldEl of dependentFieldEls) {\n                this._deleteConditionalVisibility(fieldEl);\n            }\n        }\n        const newInputEl = this.$target[0].querySelector('input');\n        if (newInputEl) {\n            newInputEl.dataset.fillWith = dataFillWith;\n        }\n    },\n    /**\n     * Sets the visibility dependency of the field.\n     *\n     * @param {string} value name of the dependency input\n     */\n     _setVisibilityDependency(value) {\n        delete this.$target[0].dataset.visibilityCondition;\n        delete this.$target[0].dataset.visibilityComparator;\n        this.rerender = true;\n        this.$target[0].dataset.visibilityDependency = value;\n    },\n    /**\n     * @private\n     */\n    _getListItems: function () {\n        const select = this._getSelect();\n        const multipleInputs = this._getMultipleInputs();\n        let options = [];\n        if (select) {\n            options = [...select.querySelectorAll('option')];\n            if (\n                this.__getListItems_forWidgetState &&\n                options.length &&\n                options[0].value === \"\" &&\n                options[0].textContent === \"\" &&\n                options[0].selected === true\n            ) {\n                options.shift();\n            }\n        } else if (multipleInputs) {\n            options = [...multipleInputs.querySelectorAll('.checkbox input, .radio input')];\n        }\n        return options.map(opt => {\n            const name = select ? opt : opt.nextElementSibling;\n            return {\n                id: /^-?[0-9]{1,15}$/.test(opt.value) ? parseInt(opt.value) : opt.value,\n                display_name: name.textContent.trim(),\n                selected: select ? opt.selected : opt.checked,\n            };\n        });\n    },\n    /**\n     * Returns the select element if it exist else null\n     *\n     * @private\n     * @returns {HTMLElement}\n     */\n    _getSelect: function () {\n        return this.$target[0].querySelector('select');\n    },\n});\n\noptions.registry.AddFieldForm = FormEditor.extend({\n    isTopOption: true,\n    isTopFirstOption: true,\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add a char field at the end of the form.\n     * New field is set as active\n     */\n    addField: async function (previewMode, value, params) {\n        const field = this._getCustomField('char', 'Custom Text');\n        field.formatInfo = this._getDefaultFormat();\n        const fieldEl = this._renderField(field);\n        this.$target.find('.s_website_form_submit, .s_website_form_recaptcha').first().before(fieldEl);\n        this.trigger_up('activate_snippet', {\n            $snippet: $(fieldEl),\n        });\n    },\n});\n\noptions.registry.AddField = FieldEditor.extend({\n    isTopOption: true,\n    isTopFirstOption: true,\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Add a char field with active field properties after the active field.\n     * New field is set as active\n     */\n    addField: async function (previewMode, value, params) {\n        this.trigger_up('option_update', {\n            optionName: 'WebsiteFormEditor',\n            name: 'add_field',\n            data: {\n                formatInfo: this._getFieldFormat(),\n                $target: this.$target,\n            },\n        });\n    },\n});\n\n// Superclass for options that need to disable a button from the snippet overlay\nconst DisableOverlayButtonOption = options.Class.extend({\n    // Disable a button of the snippet overlay\n    disableButton: function (buttonName, message) {\n        // TODO refactor in master\n        const className = 'oe_snippet_' + buttonName;\n        this.$overlay.add(this.$overlay.data('$optionsSection')).on('click', '.' + className, this.preventButton);\n        const $buttons = this.$overlay.add(this.$overlay.data('$optionsSection')).find('.' + className);\n        for (const buttonEl of $buttons) {\n            // For a disabled element to display a tooltip, it must be wrapped\n            // into a non-disabled element which holds the tooltip.\n            buttonEl.classList.add('o_disabled');\n            const spanEl = buttonEl.ownerDocument.createElement('span');\n            spanEl.setAttribute('tabindex', 0);\n            spanEl.setAttribute('title', message);\n            buttonEl.replaceWith(spanEl);\n            spanEl.appendChild(buttonEl);\n            Tooltip.getOrCreateInstance(spanEl, {delay: 0});\n        }\n    },\n\n    preventButton: function (event) {\n        // Snippet options bind their functions before the editor, so we\n        // can't cleanly unbind the editor onRemove function from here\n        event.preventDefault();\n        event.stopImmediatePropagation();\n    }\n});\n\n// Disable duplicate button for model fields\noptions.registry.WebsiteFormFieldModel = DisableOverlayButtonOption.extend({\n    start: function () {\n        this.disableButton('clone', _t('You cannot duplicate this field.'));\n        return this._super.apply(this, arguments);\n    }\n});\n\n// Disable delete button for model required fields\noptions.registry.WebsiteFormFieldRequired = DisableOverlayButtonOption.extend({\n    start: function () {\n        this.disableButton(\"remove\", _t(\n            \"This field is mandatory for this action. You cannot remove it. Try hiding it with the\"\n            + \" 'Visibility' option instead and add it a default value.\"\n        ));\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        if (!currentActionName) {\n            return;\n        }\n\n        const fieldName = this.$target[0]\n            .querySelector(\"input.s_website_form_input\").getAttribute(\"name\");\n        const spanEl = document.createElement(\"span\");\n        spanEl.innerText = _t(\"The field \u201c%(field)s\u201d is mandatory for the action \u201c%(action)s\u201d.\", {\n            field: fieldName,\n            action: currentActionName,\n        });\n        uiFragment.querySelector(\"we-alert\").appendChild(spanEl);\n    },\n});\n\n// Disable delete and duplicate button for submit\noptions.registry.WebsiteFormSubmitRequired = DisableOverlayButtonOption.extend({\n    start: function () {\n        this.disableButton('remove', _t('You can\\'t remove the submit button of the form'));\n        this.disableButton('clone', _t('You can\\'t duplicate the submit button of the form.'));\n        return this._super.apply(this, arguments);\n    }\n});\n\n// Disable \"Shown on Mobile/Desktop\" option if for an hidden field\noptions.registry.DeviceVisibility.include({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeVisibility() {\n        // Same as default but overridden by other apps\n        return await this._super(...arguments)\n            && !this.$target.hasClass('s_website_form_field_hidden');\n    },\n});\n\nexport default {\n    clearAllFormsInfo,\n};\n", "/** @odoo-module **/\n\nimport { Registry } from \"@web/core/registry\";\n\nexport default new Registry();\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport FormEditorRegistry from \"@website/js/form_editor_registry\";\n\nFormEditorRegistry.add('send_mail', {\n    formFields: [{\n        type: 'char',\n        custom: true,\n        required: true,\n        fillWith: 'name',\n        name: 'name',\n        string: _t('Your Name'),\n    }, {\n        type: 'tel',\n        custom: true,\n        fillWith: 'phone',\n        name: 'phone',\n        string: _t('Phone Number'),\n    }, {\n        type: 'email',\n        modelRequired: true,\n        fillWith: 'email',\n        name: 'email_from',\n        string: _t('Your Email'),\n    }, {\n        type: 'char',\n        custom: true,\n        fillWith: 'commercial_company_name',\n        name: 'company',\n        string: _t('Your Company'),\n    }, {\n        type: 'char',\n        modelRequired: true,\n        name: 'subject',\n        string: _t('Subject'),\n    }, {\n        type: 'text',\n        custom: true,\n        required: true,\n        name: 'description',\n        string: _t('Your Question'),\n    }],\n    fields: [{\n        name: 'email_to',\n        type: 'char',\n        required: true,\n        string: _t('Recipient Email'),\n        defaultValue: 'info@yourcompany.example.com',\n    }],\n});\n", "/** @odoo-module **/\n\nimport options from '@web_editor/js/editor/snippets.options';\n\noptions.registry.SearchBar = options.Class.extend({\n    /**\n     * @override\n     */\n    start() {\n        this.searchInputEl = this.$target[0].querySelector(\".oe_search_box\");\n        this.searchButtonEl = this.$target[0].querySelector(\".oe_search_button\");\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    setSearchType: function (previewMode, widgetValue, params) {\n        const form = this.$target.parents('form');\n        form.attr('action', params.formAction);\n\n        if (!previewMode) {\n            this.trigger_up('snippet_edition_request', {exec: () => {\n                const widget = this._requestUserValueWidgets('order_opt')[0];\n                const orderBy = widget.getValue(\"selectDataAttribute\");\n                const order = widget.$el.find(\"we-button[data-select-data-attribute='\" + orderBy + \"']\")[0];\n                if (order.classList.contains(\"d-none\")) {\n                    const defaultOrder = widget.$el.find(\"we-button[data-name='order_name_asc_opt']\")[0];\n                    defaultOrder.click(); // open\n                    defaultOrder.click(); // close\n                }\n            }});\n\n            // Reset display options.\n            const displayOptions = new Set();\n            for (const optionEl of this.$el[0].querySelectorAll('[data-dependencies=\"limit_opt\"] [data-attribute-name^=\"display\"]')) {\n                displayOptions.add(optionEl.dataset.attributeName);\n            }\n            const scopeName = this.$el[0].querySelector(`[data-set-search-type=\"${widgetValue}\"]`).dataset.name;\n            for (const displayOption of displayOptions) {\n                this.$target[0].dataset[displayOption] = this.$el[0].querySelector(\n                    `[data-attribute-name=\"${displayOption}\"][data-dependencies=\"${scopeName}\"]`\n                ) ? 'true' : '';\n            }\n        }\n    },\n\n    setOrderBy: function (previewMode, widgetValue, params) {\n        const form = this.$target.parents('form');\n        form.find(\".o_search_order_by\").attr(\"value\", widgetValue);\n    },\n    /**\n     * Sets the style of the searchbar.\n     *\n     * @see this.selectClass for parameters\n     */\n    setSearchbarStyle(previewMode, widgetValue, params) {\n        const isLight = (widgetValue === \"light\");\n        this.searchInputEl.classList.toggle(\"border-0\", isLight);\n        this.searchInputEl.classList.toggle(\"bg-light\", isLight);\n        this.searchButtonEl.classList.toggle(\"btn-light\", isLight);\n        this.searchButtonEl.classList.toggle(\"btn-primary\", !isLight);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        if (methodName === \"setSearchbarStyle\") {\n            const searchInputIsLight = this.searchInputEl.matches(\".border-0.bg-light\");\n            const searchButtonIsLight = this.searchButtonEl.matches(\".btn-light\");\n            return searchInputIsLight && searchButtonIsLight ? \"light\" : \"default\";\n        }\n        return this._super(...arguments);\n    },\n});\n\nexport default {\n    SearchBar: options.registry.SearchBar,\n};\n", "/** @odoo-module **/\n\nimport fonts from '@web_editor/js/wysiwyg/fonts';\nimport weUtils from '@web_editor/js/common/utils';\nimport options from '@web_editor/js/editor/snippets.options';\nimport { _t } from \"@web/core/l10n/translation\";\nimport { ICON_SELECTOR } from \"@web_editor/js/editor/odoo-editor/src/utils/utils\";\n\nlet dbSocialValues;\nlet dbSocialValuesProm;\nconst clearDbSocialValuesCache = () => {\n    dbSocialValuesProm = undefined;\n    dbSocialValues = undefined;\n};\nconst getDbSocialValuesCache = () => {\n    return dbSocialValues;\n};\n\noptions.registry.SocialMedia = options.Class.extend({\n    init() {\n        this._super(...arguments);\n        this.orm = this.bindService(\"orm\");\n    },\n\n    /**\n     * @override\n     */\n    start() {\n        // When the alert is clicked, focus the first media input in the editor.\n        this.__onSetupBannerClick = this._onSetupBannerClick.bind(this);\n        this.$target[0].addEventListener('click', this.__onSetupBannerClick);\n        this.entriesNotInDom = [];\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async onBuilt() {\n        await this._fetchSocialMedia();\n        for (const anchorEl of this.$target[0].querySelectorAll(':scope > a')) {\n            const mediaName = anchorEl.href.split('/website/social/').pop();\n            if (mediaName && !dbSocialValues[`social_${mediaName}`]) {\n                // Delete social media without value in DB.\n                anchorEl.remove();\n            }\n        }\n        // Ensure we do not drop a blank block.\n        this._handleNoMediaAlert();\n    },\n    /**\n     * @override\n     */\n    async cleanForSave() {\n        // When the snippet is cloned via its parent, the options UI won't be\n        // updated and DB values won't be fetched, the options `cleanForSave`\n        // will then update the website with empty values.\n        if (!dbSocialValues) {\n            return;\n        }\n        // Update the DB links.\n        let websiteId;\n        this.trigger_up('context_get', {\n            callback: function (ctx) {\n                websiteId = ctx['website_id'];\n            },\n        });\n        await this.orm.write(\"website\", [websiteId], dbSocialValues);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$target[0].removeEventListener('click', this.__onSetupBannerClick);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Applies the we-list on the target and rebuilds the social links.\n     *\n     * @see this.selectClass for parameters\n     */\n    async renderListItems(previewMode, widgetValue, params) {\n        const ariaLabelsOfSocialNetworks = {\n            \"facebook\": _t(\"Facebook\"),\n            \"twitter\": _t(\"X\"),\n            \"linkedin\": _t(\"LinkedIn\"),\n            \"youtube\": _t(\"YouTube\"),\n            \"instagram\": _t(\"Instagram\"),\n            \"github\": _t(\"GitHub\"),\n            \"tiktok\": _t(\"TikTok\"),\n        };\n        const setAriaLabelOfSocialNetwork = (el, name, url) => {\n            let ariaLabel = ariaLabelsOfSocialNetworks[name];\n            if (!ariaLabel) {\n                try {\n                    // Return the domain of the given url.\n                    ariaLabel = new URL(url).hostname.split('.').slice(-2)[0];\n                } catch {\n                    // Fallback if the url is not valid.\n                    ariaLabel = _t(\"Other social network\");\n                }\n            }\n            el.setAttribute(\"aria-label\", ariaLabel);\n        };\n\n        const anchorEls = this.$target[0].querySelectorAll(':scope > a');\n        let entries = JSON.parse(widgetValue);\n        const anchorsToRemoveEls = [];\n        for (let i = 0; i < anchorEls.length; i++) {\n            // For each position, check if the item that was there before\n            // (marked by _computeWidgetState), is still there. Otherwise,\n            // remove it. TODO improve ?\n            if (!entries.find(entry => parseInt(entry.domPosition) === i)) {\n                anchorsToRemoveEls.push(anchorEls[i]);\n            }\n        }\n        for (const el of anchorsToRemoveEls) {\n            el.remove();\n        }\n        this.entriesNotInDom = [];\n\n        for (let listPosition = 0; listPosition < entries.length; listPosition++) {\n            const entry = entries[listPosition];\n            // Check if the url is valid.\n            const url = entry.display_name;\n            if (url && !/^(([a-zA-Z]+):|\\/)/.test(url)) {\n                // We permit every protocol (http:, https:, ftp:, mailto:,...).\n                // If none is explicitly specified, we assume it is a https.\n                entry.display_name = `https://${url}`;\n            }\n            const isDbField = Boolean(entry.media);\n            if (isDbField) {\n                // Handle URL change for DB links.\n                dbSocialValues[`social_${entry.media}`] = entry.display_name;\n            }\n\n            let anchorEl = anchorEls[entry.domPosition];\n            if (entry.selected) {\n                if (!anchorEl) {\n                    if (anchorEls.length === 0) {\n                        // Create a HTML element if no one already exist.\n                        anchorEl = document.createElement('a');\n                        anchorEl.setAttribute('target', '_blank');\n                        const iEl = document.createElement('i');\n                        iEl.classList.add('fa', 'rounded-circle', 'shadow-sm', 'o_editable_media');\n                        anchorEl.appendChild(iEl);\n                    } else {\n                        // Copy existing style if there is already another link.\n                        anchorEl = this.$target[0].querySelector(':scope > a').cloneNode(true);\n                        this._removeSocialMediaClasses(anchorEl);\n                    }\n                    const iEl = anchorEl.querySelector(ICON_SELECTOR);\n                    if (iEl) {\n                        const faIcon = isDbField ? `fa-${entry.media}` : 'fa-pencil';\n                        iEl.classList.add(faIcon);\n                    }\n                    if (isDbField) {\n                        anchorEl.href = `/website/social/${encodeURIComponent(entry.media)}`;\n                        anchorEl.classList.add(`s_social_media_${entry.media}`);\n                    }\n                    setAriaLabelOfSocialNetwork(anchorEl, entry.media, entry.display_name);\n                }\n            } else {\n                if (anchorEl) {\n                    delete entry.domPosition;\n                    anchorEl.remove();\n                }\n                entry.listPosition = listPosition;\n                this.entriesNotInDom.push(entry);\n                continue;\n            }\n            if (!isDbField) {\n                // Handle URL change for custom links.\n                const href = anchorEl.getAttribute('href');\n                if (href !== entry.display_name) {\n                    let socialMedia = null;\n                    if (this._isValidURL(entry.display_name)) {\n                        // Propose an icon only for valid URLs (no mailto).\n                        socialMedia = this._findRelevantSocialMedia(entry.display_name);\n                        if (socialMedia) {\n                            const iEl = anchorEl.querySelector(ICON_SELECTOR);\n                            this._removeSocialMediaClasses(anchorEl);\n                            anchorEl.classList.add(`s_social_media_${socialMedia}`);\n                            if (iEl) {\n                                iEl.classList.add(`fa-${socialMedia}`);\n                            }\n                        }\n                    }\n                    anchorEl.setAttribute('href', entry.display_name);\n                    setAriaLabelOfSocialNetwork(anchorEl, socialMedia, entry.display_name);\n                }\n            }\n            // Place the link at the correct position\n            this.$target[0].appendChild(anchorEl);\n        }\n\n        // Restore whitespaces around the links\n        this.$target[0].normalize();\n        const finalLinkEls = this.$target[0].querySelectorAll(':scope > a');\n        if (finalLinkEls.length) {\n            finalLinkEls[0].previousSibling.textContent = '\\n';\n            for (const linkEl of finalLinkEls) {\n                linkEl.after(document.createTextNode('\\n'));\n            }\n        }\n\n        this._handleNoMediaAlert();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async _computeWidgetState(methodName, params) {\n        if (methodName !== 'renderListItems') {\n            return this._super(methodName, params);\n        }\n        await this._fetchSocialMedia();\n        let listPosition = 0;\n        let domPosition = 0;\n        // Check the DOM to compute the state of the ListUserValueWidget.\n        let entries = [...this.$target[0].querySelectorAll(':scope > a')].map(el => {\n            const media = el.href.split('/website/social/')[1];\n            // Avoid a DOM entry and a non-dom entry having the same position.\n            while (this.entriesNotInDom.find(entry => entry.listPosition === listPosition)) {\n                listPosition++;\n            }\n            return {\n                id: weUtils.generateHTMLId(),\n                display_name: media ? dbSocialValues[`social_${media}`] : el.getAttribute('href'),\n                placeholder: `https://${encodeURIComponent(media) || 'example'}.com/yourPage`,\n                undeletable: !!media,\n                notToggleable: !media,\n                selected: true,\n                listPosition: listPosition++,\n                domPosition: domPosition++,\n                media: media,\n            };\n        });\n        // Adds the DB social media links that are not in the DOM.\n        for (let [media, link] of Object.entries(dbSocialValues)) {\n            media = media.split('social_').pop();\n            if (!this.$target[0].querySelector(`:scope > a[href=\"/website/social/${encodeURIComponent(media)}\"]`)) {\n                const entryNotInDom = this.entriesNotInDom.find(entry => entry.media === media);\n                if (!entryNotInDom) {\n                    this.entriesNotInDom.push({\n                        id: weUtils.generateHTMLId(),\n                        display_name: link,\n                        placeholder: `https://${encodeURIComponent(media)}.com/yourPage`,\n                        undeletable: true,\n                        selected: false,\n                        listPosition: listPosition++,\n                        media: media,\n                        notToggleable: false,\n                    });\n                } else {\n                    // Do not change the listPosition of the existing entry.\n                    entryNotInDom.display_name = link;\n                    entryNotInDom.undeletable = true;\n                    entryNotInDom.notToggleable = false;\n                }\n            }\n        }\n        // Reorder entries and entriesNotInDom by position.\n        entries = entries.concat(this.entriesNotInDom);\n        entries.sort((a, b) => {\n            return a.listPosition - b.listPosition;\n        });\n        return JSON.stringify(entries);\n    },\n    /**\n     * Fetches the urls of the social networks that are in the database.\n     */\n    async _fetchSocialMedia() {\n        if (!dbSocialValuesProm) {\n            let websiteId;\n            this.trigger_up('context_get', {\n                callback: function (ctx) {\n                    websiteId = ctx['website_id'];\n                },\n            });\n            // Fetch URLs for db links.\n            dbSocialValuesProm = this.orm.read(\"website\", [websiteId], [\n                \"social_facebook\",\n                \"social_twitter\",\n                \"social_linkedin\",\n                \"social_youtube\",\n                \"social_instagram\",\n                \"social_github\",\n                \"social_tiktok\",\n            ]).then(function (values) {\n                [dbSocialValues] = values;\n                delete dbSocialValues.id;\n            });\n        }\n        await dbSocialValuesProm;\n    },\n    /**\n     * Finds the social network for the given url.\n     *\n     * @param {String} url\n     * @return {String} The social network to which the url leads to.\n     */\n    _findRelevantSocialMedia(url) {\n        // Note that linkedin, twitter, github and tiktok will also work because\n        // the url will match the good icon so we don't need a specific regex.\n        const supportedSocialMedia = [\n            ['facebook', /^(https?:\\/\\/)(www\\.)?(facebook|fb|m\\.facebook)\\.(com|me).*$/],\n            ['youtube', /^(https?:\\/\\/)(www\\.)?(youtube.com|youtu.be).*$/],\n            ['instagram', /^(https?:\\/\\/)(www\\.)?(instagram.com|instagr.am|instagr.com).*$/],\n        ];\n        for (const [socialMedia, regex] of supportedSocialMedia) {\n            if (regex.test(url)) {\n                return socialMedia;\n            }\n        }\n        // Check if an icon matches the URL domain\n        try {\n            const domain = new URL(url).hostname.split('.').slice(-2)[0];\n            fonts.computeFonts();\n            const iconNames = fonts.fontIcons[0].alias;\n            const exactIcon = iconNames.find(el => el === `fa-${domain}`);\n            return (exactIcon || iconNames.find(el => el.includes(domain))).split('fa-').pop();\n        } catch {\n            return false;\n        }\n    },\n    /**\n     * Adds a warning banner to alert that there are no social networks.\n     */\n    _handleNoMediaAlert() {\n        const alertEl = this.$target[0].querySelector('div.css_non_editable_mode_hidden');\n        if (this.$target[0].querySelector(':scope > a:not(.d-none)')) {\n            if (alertEl) {\n                alertEl.remove();\n            }\n        } else {\n            if (!alertEl) {\n                // Create the alert banner.\n                const divEl = document.createElement('div');\n                const classes = ['alert', 'alert-info', 'css_non_editable_mode_hidden', 'text-center'];\n                divEl.classList.add(...classes);\n                const spanEl = document.createElement('span');\n                spanEl.textContent = _t(\"Click here to setup your social networks\");\n                this.$target[0].appendChild(divEl).append(spanEl);\n            }\n        }\n    },\n    /**\n     * @param  {String} str\n     * @returns {boolean} is the string a valid URL.\n     */\n    _isValidURL(str) {\n        let url;\n        try {\n            url = new URL(str);\n        } catch {\n            return false;\n        }\n        return url.protocol.startsWith('http');\n    },\n    /**\n     * Removes social media classes from the given element.\n     *\n     * @param  {HTMLElement} anchorEl\n     */\n    _removeSocialMediaClasses(anchorEl) {\n        let regx = new RegExp('\\\\b' + 's_social_media_' + '[^1-9][^ ]*[ ]?\\\\b');\n        anchorEl.className = anchorEl.className.replace(regx, '');\n        const iEl = anchorEl.querySelector(ICON_SELECTOR);\n        if (iEl) {\n            regx = new RegExp('\\\\b' + 'fa-' + '[^1-9][^ ]*[ ]?\\\\b');\n            // Remove every fa classes except fa-x sizes.\n            iEl.className = iEl.className.replace(regx, '');\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onSetupBannerClick(ev) {\n        if (ev.target.closest('div.css_non_editable_mode_hidden')) {\n            // TODO if the options are not already instantiated, this won't\n            // work of course\n            this._requestUserValueWidgets('social_media_list')[0].focus();\n        }\n    },\n});\n\nexport default {\n    SocialMedia: options.registry.SocialMedia,\n    clearDbSocialValuesCache,\n    getDbSocialValuesCache,\n};\n", "/** @odoo-module **/\n\nimport options from '@web_editor/js/editor/snippets.options';\nimport weUtils from '@web_editor/js/common/utils';\n\noptions.registry.StepsConnector = options.Class.extend({\n    /**\n     * @override\n     */\n    start() {\n        this.$target.on('content_changed.StepsConnector', () => this._reloadConnectors());\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        this._super(...arguments);\n        this.$target.off('.StepsConnector');\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    selectClass: function (previewMode, value, params) {\n        this._super(...arguments);\n        if (params.name === 'connector_type') {\n            this._reloadConnectors();\n            let markerEnd = '';\n            if (['s_process_steps_connector_arrow', 's_process_steps_connector_curved_arrow'].includes(value)) {\n                const arrowHeadEl = this.$target[0].querySelector('.s_process_steps_arrow_head');\n                // The arrowhead id is set here so that they are different per snippet.\n                if (!arrowHeadEl.id) {\n                    arrowHeadEl.id = 's_process_steps_arrow_head' + Date.now();\n                }\n                markerEnd = `url(#${arrowHeadEl.id})`;\n            }\n            this.$target[0].querySelectorAll('.s_process_step_connector path').forEach(path => path.setAttribute('marker-end', markerEnd));\n        }\n    },\n    /**\n     * Changes arrow heads' fill color.\n     *\n     * @see this.selectClass for parameters\n     */\n    changeColor(previewMode, widgetValue, params) {\n        const htmlPropColor = weUtils.getCSSVariableValue(widgetValue);\n        const arrowHeadEl = this.$target[0].closest('.s_process_steps').querySelector('.s_process_steps_arrow_head');\n        arrowHeadEl.querySelector('path').style.fill = htmlPropColor || widgetValue;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    notify(name) {\n        if (['change_column_size', 'change_container_width', 'change_columns', 'move_snippet'].includes(name)) {\n            this._reloadConnectors();\n        } else {\n            this._super(...arguments);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeVisibility() {\n        // We don't use the service_context_get intentionally because the\n        // connectors are hidden as soon as the page is smaller than 992px\n        // (the BS lg breakpoint).\n        const isMobileView = weUtils.isMobileView(this.$target[0]);\n        return !isMobileView && this._super(...arguments);\n    },\n    /**\n     * Width and position of the connectors should be updated when one of the\n     * steps is modified.\n     *\n     * @private\n     */\n    _reloadConnectors() {\n        const possibleTypes = this._requestUserValueWidgets('connector_type')[0].getMethodsParams().optionsPossibleValues.selectClass;\n        const type = possibleTypes.find(possibleType => possibleType && this.$target[0].classList.contains(possibleType)) || '';\n        // As the connectors are only visible in desktop, we can ignore the\n        // steps that are only visible in mobile.\n        const stepsEls = this.$target[0].querySelectorAll('.s_process_step:not(.o_snippet_desktop_invisible)');\n        const nbBootstrapCols = 12;\n        let colsInRow = 0;\n\n        for (let i = 0; i < stepsEls.length - 1; i++) {\n            const connectorEl = stepsEls[i].querySelector('.s_process_step_connector');\n            const stepMainElementRect = this._getStepMainElementRect(stepsEls[i]);\n            const nextStepMainElementRect = this._getStepMainElementRect(stepsEls[i + 1]);\n            const stepSize = this._getClassSuffixedInteger(stepsEls[i], 'col-lg-');\n            const nextStepSize = this._getClassSuffixedInteger(stepsEls[i + 1], 'col-lg-');\n            const stepOffset = this._getClassSuffixedInteger(stepsEls[i], 'offset-lg-');\n            const nextStepOffset = this._getClassSuffixedInteger(stepsEls[i + 1], 'offset-lg-');\n            const stepPaddingTop = this._getClassSuffixedInteger(stepsEls[i], 'pt');\n            const nextStepPaddingTop = this._getClassSuffixedInteger(stepsEls[i + 1], 'pt');\n\n            connectorEl.style.left = `calc(50% + ${stepMainElementRect.width / 2}px + 16px)`;\n            connectorEl.style.height = `${stepMainElementRect.height}px`;\n            connectorEl.style.width = `calc(${100 * (stepSize / 2 + nextStepOffset + nextStepSize / 2) / stepSize}% - ${stepMainElementRect.width / 2}px - ${nextStepMainElementRect.width / 2}px - 32px)`;\n\n            const isTheLastColOfRow = nbBootstrapCols <\n                colsInRow + stepSize + stepOffset + nextStepSize + nextStepOffset;\n            const isNextStepTooLow = stepMainElementRect.height + stepPaddingTop <\n                nextStepPaddingTop;\n            connectorEl.classList.toggle('d-none', isTheLastColOfRow || isNextStepTooLow);\n            colsInRow = isTheLastColOfRow ? 0 : colsInRow + stepSize + stepOffset;\n            // When we are mobile view, the connector is not visible, here we\n            // display it quickly just to have its size.\n            connectorEl.style.display = 'block';\n            const {height, width} = connectorEl.getBoundingClientRect();\n            connectorEl.style.removeProperty('display');\n            connectorEl.setAttribute('viewBox', `0 0 ${width} ${height}`);\n            connectorEl.querySelector('path').setAttribute('d', this._getPath(type, width, height));\n        }\n    },\n    /**\n     * Returns the number suffixed to the class given in parameter.\n     *\n     * @private\n     * @param {HTMLElement} el\n     * @param {String} classNamePrefix\n     * @returns {Integer}\n     */\n    _getClassSuffixedInteger(el, classNamePrefix) {\n        const className = [...el.classList].find(cl => cl.startsWith(classNamePrefix));\n        return className ? parseInt(className.replace(classNamePrefix, '')) : 0;\n    },\n    /**\n     * Returns the step's icon or content bounding rectangle.\n     *\n     * @private\n     * @param {HTMLElement}\n     * @returns {object}\n     */\n    _getStepMainElementRect(stepEl) {\n        const iconEl = stepEl.querySelector(\".s_process_step_number\");\n        if (iconEl) {\n            return iconEl.getBoundingClientRect();\n        }\n        const contentEls = stepEl.querySelectorAll('.s_process_step_content > *');\n        // If there is no icon, the biggest text bloc in the content container\n        // will be chosen.\n        if (contentEls.length) {\n            const contentRects = [...contentEls].map(contentEl => {\n                const range = document.createRange();\n                range.selectNodeContents(contentEl);\n                return range.getBoundingClientRect();\n            });\n            return contentRects.reduce((previous, current) => {\n                return current.width > previous.width ? current : previous;\n            });\n        }\n        return {};\n    },\n    /**\n     * Returns the svg path based on the type of connector.\n     *\n     * @private\n     * @param {string} type\n     * @param {integer} width\n     * @param {integer} height\n     * @returns {string}\n     */\n    _getPath(type, width, height) {\n        const hHeight = height / 2;\n        switch (type) {\n            case 's_process_steps_connector_line': {\n                return `M 0 ${hHeight} L ${width} ${hHeight}`;\n            }\n            case 's_process_steps_connector_arrow': {\n                return `M ${0.05 * width} ${hHeight} L ${0.95 * width - 6} ${hHeight}`;\n            }\n            case 's_process_steps_connector_curved_arrow': {\n                return `M ${0.05 * width} ${hHeight * 1.2} Q ${width / 2} ${hHeight * 1.8}, ${0.95 * width - 6} ${hHeight * 1.2}`;\n            }\n        }\n        return '';\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport { browser } from \"@web/core/browser/browser\";\nimport { patch } from \"@web/core/utils/patch\";\n\nimport { LinkPopoverWidget } from '@web_editor/js/wysiwyg/widgets/link_popover_widget';\n\n\n\npatch(LinkPopoverWidget.prototype, {\n    /**\n     * @override\n     */\n    start() {\n        // hide popover while typing on mega menu\n        if (this.target.closest('.o_mega_menu')) {\n            let timeoutID = undefined;\n            this.$target.on('keydown.link_popover', () => {\n                this.$target.popover('hide');\n                clearTimeout(timeoutID);\n                timeoutID = setTimeout(() => this.$target.popover('show'), 1500);\n            });\n        }\n        this.$el.on('click', '.o_we_full_url, .o_we_url_link', this._onPreviewLinkClick.bind(this));\n\n        return super.start(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Opens website page links in backend mode by forcing the '/@/' controller.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    async _onPreviewLinkClick(ev) {\n        if (this.target.href) {\n            const currentUrl = new URL(this.target.href);\n            if (window.location.hostname === currentUrl.hostname && !currentUrl.pathname.startsWith('/@/')) {\n                ev.preventDefault();\n                currentUrl.pathname = `/@${currentUrl.pathname}`;\n                browser.open(currentUrl);\n            }\n        }\n    }\n});\n\nexport class NavbarLinkPopoverWidget extends LinkPopoverWidget {\n    constructor(params) {\n        super(...arguments);\n        this.checkIsWebsiteDesigner = params.checkIsWebsiteDesigner;\n        this.onEditLinkClick = params.onEditLinkClick;\n        this.onEditMenuClick = params.onEditMenuClick;\n    }\n    /**\n     *\n     * @override\n     */\n    async start() {\n        this.isWebsiteDesigner = await this.checkIsWebsiteDesigner();\n        const $removeLink = this.$el.find('.o_we_remove_link');\n        // remove link has no sense on navbar menu links, instead show edit menu\n        if (this.isWebsiteDesigner) {\n            const $anchor = $('<a/>', {\n                href: '#', class: 'ms-2 js_edit_menu', title: _t('Edit Menu'),\n                'data-bs-placement': 'top', 'data-bs-toggle': 'tooltip',\n            }).append($('<i/>', {class: 'fa fa-sitemap'}));\n            $removeLink.replaceWith($anchor);\n            $anchor.on('click', () => this.onEditMenuClick(this));\n        } else {\n            this.$el.find('.o_we_edit_link').remove();\n            $removeLink.remove();\n        }\n\n        return super.start(...arguments);\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Opens the menu item editor.\n     *\n     * @override\n     * @param {Event} ev\n     */\n    _onEditLinkClick(ev) {\n        this.onEditLinkClick(this);\n    }\n}\n", "/** @odoo-module **/\n\n// The goal of this patch is to handle some website-specific behavior when\n// executing editor commands on DOM elements.\nimport { UNMERGEABLE_SELECTORS } from \"@web_editor/js/editor/odoo-editor/src/utils/sanitize\";\n\nUNMERGEABLE_SELECTORS.push(\"o_text_highlight_item\");\n\n/**\n * Used to prevent handling the text highlight SVG the same way as text\n * content on backward deletion.\n */\nHTMLElement.prototype.oDeleteBackwardOdooEditor = HTMLElement.prototype.oDeleteBackward;\nHTMLElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false, offsetLimit) {\n    const leftNode = this.childNodes[offset - 1];\n    if (offset && leftNode) {\n        // Some elements like text highlight SVGs should be ignored.\n        if (leftNode.classList && leftNode.classList.contains(\"o_text_highlight_svg\")) {\n            return;\n        }\n    }\n    this.oDeleteBackwardOdooEditor(...arguments);\n};\n", "/** @odoo-module **/\n\nimport { OdooEditor } from \"@web_editor/js/editor/odoo-editor/src/OdooEditor\";\nimport { patch } from \"@web/core/utils/patch\";\nimport { removeTextHighlight } from \"@website/js/text_processing\";\n\n/**\n * The goal of this patch is to correctly handle OdooEditor's behaviour for text\n * highlight elements.\n */\npatch(OdooEditor.prototype, {\n    /**\n     * @override\n     */\n    _onClipboardCopy(e) {\n        super._onClipboardCopy(e);\n\n        const selection = this.document.getSelection();\n        const range = selection.getRangeAt(0);\n        let rangeContent = range.cloneContents();\n        const firstChild = rangeContent.firstChild;\n\n        // Fix the copied range and remove the highlight units when the content\n        // is partially selected.\n        if (firstChild && firstChild.className && firstChild.className.includes(\"o_text_highlight_item\")) {\n            const textHighlightEl = range.commonAncestorContainer.cloneNode();\n            textHighlightEl.replaceChildren(...rangeContent.childNodes);\n            removeTextHighlight(textHighlightEl);\n            rangeContent = textHighlightEl;\n            const data = document.createElement(\"data\");\n            data.append(rangeContent);\n            const html = data.innerHTML;\n            e.clipboardData.setData(\"text/plain\", selection.toString());\n            e.clipboardData.setData(\"text/html\", html);\n            e.clipboardData.setData(\"text/odoo-editor\", html);\n        }\n    },\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport FormEditorRegistry from \"@website/js/form_editor_registry\";\n\nFormEditorRegistry.add('create_lead', {\n    formFields: [{\n        type: 'char',\n        required: true,\n        name: 'contact_name',\n        fillWith: 'name',\n        string: _t('Your Name'),\n    }, {\n        type: 'tel',\n        name: 'phone',\n        fillWith: 'phone',\n        string: _t('Phone Number'),\n    }, {\n        type: 'email',\n        required: true,\n        fillWith: 'email',\n        name: 'email_from',\n        string: _t('Your Email'),\n    }, {\n        type: 'char',\n        required: true,\n        fillWith: 'commercial_company_name',\n        name: 'partner_name',\n        string: _t('Your Company'),\n    }, {\n        type: 'char',\n        modelRequired: true,\n        name: 'name',\n        string: _t('Subject'),\n    }, {\n        type: 'text',\n        required: true,\n        name: 'description',\n        string: _t('Your Question'),\n    }],\n    fields: [{\n        name: 'team_id',\n        type: 'many2one',\n        relation: 'crm.team',\n        domain: [['use_opportunities', '=', true]],\n        string: _t('Sales Team'),\n        title: _t('Assign leads/opportunities to a sales team.'),\n    }, {\n        name: 'user_id',\n        type: 'many2one',\n        relation: 'res.users',\n        domain: [['share', '=', false]],\n        string: _t('Salesperson'),\n        title: _t('Assign leads/opportunities to a salesperson.'),\n    }],\n});\n", "/** @odoo-module **/\n\nimport { _t } from \"@web/core/l10n/translation\";\nimport FormEditorRegistry from \"@website/js/form_editor_registry\";\n\nFormEditorRegistry.add('create_ticket', {\n    formFields: [{\n        type: 'char',\n        required: true,\n        name: 'partner_name',\n        fillWith: 'name',\n        string: _t('Full Name'),\n    }, {\n        type: 'tel',\n        name: 'partner_phone',\n        fillWith: 'phone',\n        string: _t('Phone Number'),\n    }, {\n        type: 'email',\n        required: true,\n        name: 'partner_email',\n        fillWith: 'email',\n        string: _t('Email Address'),\n    }, {\n        type: 'char',\n        fillWith: 'commercial_company_name',\n        name: 'partner_company_name',\n        string: _t('Company Name'),\n    }, {\n        type: 'char',\n        modelRequired: true,\n        name: 'name',\n        string: _t('Message Subject'),\n    }, {\n        type: 'text',\n        required: true,\n        name: 'description',\n        string: _t('Ask Your Question'),\n    }, {\n        type: 'binary',\n        custom: true,\n        name: _t('Attach File'),\n    }],\n    fields: [{\n        name: 'team_id',\n        type: 'many2one',\n        relation: 'helpdesk.team',\n        string: _t('Helpdesk Team'),\n    }],\n    successPage: '/your-ticket-has-been-submitted',\n});\n", "/** @odoo-module **/\n\nimport { renderToElement } from \"@web/core/utils/render\";\nimport options from '@web_editor/js/editor/snippets.options';\nimport { _t } from \"@web/core/l10n/translation\";\n\noptions.registry.Donation = options.Class.extend({\n    /**\n     * @override\n     */\n    start() {\n        this.defaultDescription = _t(\"Add a description here\");\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onBuilt() {\n        this._rebuildPrefilledOptions();\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    cleanForSave() {\n        if (!this.$target[0].dataset.descriptions) {\n            this._updateDescriptions();\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    async updateUI() {\n        await this._super(...arguments);\n        this._buildDescriptionsList();\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Show/hide options in the page.\n     *\n     * @see this.selectClass for parameters\n     */\n    displayOptions(previewMode, widgetValue, params) {\n        this.$target[0].dataset.displayOptions = widgetValue;\n        if (!widgetValue && this.$target[0].dataset.customAmount === \"slider\") {\n            this.$target[0].dataset.customAmount = \"freeAmount\";\n        } else if (widgetValue && !this.$target[0].dataset.prefilledOptions) {\n            this.$target[0].dataset.customAmount = \"slider\";\n        }\n        this._rebuildPrefilledOptions();\n    },\n    /**\n     * Add/remove prefilled buttons.\n     *\n     * @see this.selectClass for parameters\n     */\n    togglePrefilledOptions(previewMode, widgetValue, params) {\n        this.$target[0].dataset.prefilledOptions = widgetValue;\n        this.$el.find('.o_we_prefilled_options_list').toggleClass('d-none', !widgetValue);\n        if (!widgetValue && this.$target[0].dataset.displayOptions) {\n            this.$target[0].dataset.customAmount = \"slider\";\n        }\n        this._rebuildPrefilledOptions();\n    },\n    /**\n     * Add/remove description of prefilled buttons.\n     *\n     * @see this.selectClass for parameters\n     */\n    toggleOptionDescription(previewMode, widgetValue, params) {\n        this.$target[0].dataset.descriptions = widgetValue;\n        this.renderListItems(false, this._buildPrefilledOptionsList());\n    },\n    /**\n     * Select an amount input\n     *\n     * @see this.selectClass for parameters\n     */\n    selectAmountInput(previewMode, widgetValue, params) {\n        this.$target[0].dataset.customAmount = widgetValue;\n        this._rebuildPrefilledOptions();\n    },\n    /**\n     * Apply the we-list on the target and rebuild the input(s)\n     *\n     * @see this.selectClass for parameters\n     */\n    renderListItems(previewMode, value, params) {\n        const valueList = JSON.parse(value);\n        const donationAmounts = [];\n        delete this.$target[0].dataset.donationAmounts;\n        valueList.forEach((value) => {\n            donationAmounts.push(value.display_name);\n        });\n        this.$target[0].dataset.donationAmounts = JSON.stringify(donationAmounts);\n        this._rebuildPrefilledOptions();\n    },\n    /**\n     * Redraws the target whenever the list changes\n     *\n     * @see this.selectClass for parameters\n     */\n    listChanged(previewMode, value, params) {\n        this._updateDescriptions();\n        this._rebuildPrefilledOptions();\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setMinimumAmount(previewMode, widgetValue, params) {\n        this.$target[0].dataset.minimumAmount = widgetValue;\n        const $rangeSlider = this.$('#s_donation_range_slider');\n        const $amountInput = this.$('#s_donation_amount_input');\n        if ($rangeSlider.length) {\n            $rangeSlider[0].min = widgetValue;\n        } else if ($amountInput.length) {\n            $amountInput[0].min = widgetValue;\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setMaximumAmount(previewMode, widgetValue, params) {\n        this.$target[0].dataset.maximumAmount = widgetValue;\n        const $rangeSlider = this.$('#s_donation_range_slider');\n        const $amountInput = this.$('#s_donation_amount_input');\n        if ($rangeSlider.length) {\n            $rangeSlider[0].max = widgetValue;\n        } else if ($amountInput.length) {\n            $amountInput[0].max = widgetValue;\n        }\n    },\n    /**\n     * @see this.selectClass for parameters\n     */\n    setSliderStep(previewMode, widgetValue, params) {\n        this.$target[0].dataset.sliderStep = widgetValue;\n        const $rangeSlider = this.$('#s_donation_range_slider');\n        if ($rangeSlider.length) {\n            $rangeSlider[0].step = widgetValue;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState(methodName, params) {\n        switch (methodName) {\n            case 'displayOptions': {\n                return this.$target[0].dataset.displayOptions;\n            }\n            case 'togglePrefilledOptions': {\n                return this.$target[0].dataset.prefilledOptions;\n            }\n            case 'toggleOptionDescription': {\n                return this.$target[0].dataset.descriptions;\n            }\n            case 'selectAmountInput': {\n                return this.$target[0].dataset.customAmount;\n            }\n            case 'renderListItems': {\n                return this._buildPrefilledOptionsList();\n            }\n            case 'setMinimumAmount': {\n                return this.$target[0].dataset.minimumAmount;\n            }\n            case 'setMaximumAmount': {\n                return this.$target[0].dataset.maximumAmount;\n            }\n            case 'setSliderStep': {\n                return this.$target[0].dataset.sliderStep;\n            }\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'free_amount_opt') {\n            return !(this.$target[0].dataset.displayOptions && !this.$target[0].dataset.prefilledOptions);\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _renderCustomXML(uiFragment) {\n        const list = document.createElement('we-list');\n        list.dataset.dependencies = \"pre_filled_opt\";\n        list.dataset.addItemTitle = _t(\"Add new pre-filled option\");\n        list.dataset.renderListItems = '';\n        list.dataset.unsortable = 'true';\n        list.dataset.inputType = 'number';\n        list.dataset.defaultValue = 50;\n        list.dataset.listChanged = '';\n        $(uiFragment).find('we-checkbox[data-name=\"pre_filled_opt\"]').after(list);\n    },\n    /**\n     * Build the prefilled options list in the editor panel\n     *\n     * @private\n     */\n    _buildPrefilledOptionsList() {\n        const amounts = JSON.parse(this.$target[0].dataset.donationAmounts);\n        let valueList = amounts.map(amount => {\n            return {\n                id: amount,\n                display_name: amount,\n            };\n        });\n        return JSON.stringify(valueList);\n    },\n    /**\n     * Add descriptions in the prefilled options list of the\n     * editor panel.\n     *\n     * @private\n     */\n    _buildDescriptionsList() {\n        if (this.$target[0].dataset.descriptions) {\n            const $descriptions = this.$target.find('#s_donation_description_inputs > input');\n            const $tableEl = this.$el.find('we-list table');\n            $tableEl.find(\"tr\").toArray().forEach((trEl, i) => {\n                const $inputAmount = $(trEl).find('td').first();\n                $inputAmount.addClass('w-25');\n                const tdEl = document.createElement('td');\n                const inputEl = document.createElement('input');\n                inputEl.type = 'text';\n                inputEl.value = $descriptions[i] ? $descriptions[i].value : this.defaultDescription;\n                tdEl.classList.add('w-auto');\n                tdEl.appendChild(inputEl);\n                $(tdEl).insertAfter($inputAmount);\n            });\n            this._updateDescriptions();\n        }\n    },\n    /**\n     * Update descriptions in the input hidden.\n     *\n     * @private\n     */\n    _updateDescriptions() {\n        const descriptionInputs = this.$target.find('#s_donation_description_inputs');\n        descriptionInputs.empty();\n        const descriptions = this.$el.find('we-list input[type=text]');\n        descriptions.toArray().forEach((description) => {\n            const inputEl = document.createElement('input');\n            inputEl.type = 'hidden';\n            inputEl.classList.add('o_translatable_input_hidden', 'd-block', 'mb-1', 'w-100');\n            inputEl.name = 'donation_descriptions';\n            inputEl.value = description.value;\n            descriptionInputs[0].appendChild(inputEl);\n        });\n    },\n    /**\n     * Rebuild options in the DOM.\n     *\n     * @private\n     */\n    _rebuildPrefilledOptions() {\n        const rebuild = this.$target[0].dataset.displayOptions;\n        this.$target.find('.s_donation_prefilled_buttons').remove();\n        const layout = this.$target[0].dataset.customAmount;\n        const $slider = this.$target.find('.s_donation_range_slider_wrap');\n        if (layout !== \"slider\" || !rebuild) {\n            $slider.remove();\n        }\n        if (rebuild) {\n            if (layout === \"slider\" && !$slider.length) {\n                const sliderTemplate = $(renderToElement('website_payment.donation.slider', {\n                    minimum_amount: this.$target[0].dataset.minimumAmount,\n                    maximum_amount: this.$target[0].dataset.maximumAmount,\n                    slider_step: this.$target[0].dataset.sliderStep,\n                }));\n                this.$target.find('.s_donation_donate_btn').before(sliderTemplate);\n            }\n            const prefilledOptions = this.$target[0].dataset.prefilledOptions;\n            let donationAmounts = [];\n            let showDescriptions = false;\n            if (prefilledOptions) {\n                donationAmounts = JSON.parse(this.$target[0].dataset.donationAmounts);\n                showDescriptions = this.$target[0].dataset.descriptions;\n                if (showDescriptions) {\n                    const $descriptions = this.$target.find('#s_donation_description_inputs > input');\n                    donationAmounts = donationAmounts.map((amount, i) => {\n                        return {\n                            value: amount,\n                            description: $descriptions[i] ? $descriptions[i].value : this.defaultDescription,\n                        };\n                    });\n                }\n            }\n            const $prefilledButtons = $(renderToElement(`website_payment.donation.prefilledButtons${showDescriptions ? 'Descriptions' : ''}`, {\n                prefilled_buttons: donationAmounts,\n                custom_input: layout === \"freeAmount\",\n                minimum_amount: this.$target[0].dataset.minimumAmount,\n            }));\n            this.$target.find('#s_donation_description_inputs').after($prefilledButtons);\n        }\n    },\n});\n\nexport default {\n    Donation: options.registry.Donation,\n};\n"], "file": "/web/assets/1/90984f7/website.assets_all_wysiwyg.js", "sourceRoot": "../../../../"}