)]}'
{"version": 3, "sources": ["/web_editor/static/lib/cropperjs/cropper.js", "/web_editor/static/lib/jquery-cropper/jquery-cropper.js", "/web_editor/static/lib/jQuery.transfo.js", "/web/static/lib/nearest/jquery.nearest.js", "/web_editor/static/lib/webgl-image-filter/webgl-image-filter.js", "/web_editor/static/lib/odoo-editor/src/OdooEditor.js", "/web_editor/static/lib/odoo-editor/src/utils/constants.js", "/web_editor/static/lib/odoo-editor/src/utils/sanitize.js", "/web_editor/static/lib/odoo-editor/src/utils/serialize.js", "/web_editor/static/lib/odoo-editor/src/utils/DOMPurify.js", "/web_editor/static/lib/odoo-editor/src/tablepicker/TablePicker.js", "/web_editor/static/lib/odoo-editor/src/powerbox/patienceDiff.js", "/web_editor/static/lib/odoo-editor/src/powerbox/Powerbox.js", "/web_editor/static/lib/odoo-editor/src/commands/align.js", "/web_editor/static/lib/odoo-editor/src/commands/commands.js", "/web_editor/static/lib/odoo-editor/src/commands/deleteBackward.js", "/web_editor/static/lib/odoo-editor/src/commands/deleteForward.js", "/web_editor/static/lib/odoo-editor/src/commands/enter.js", "/web_editor/static/lib/odoo-editor/src/commands/shiftEnter.js", "/web_editor/static/lib/odoo-editor/src/commands/shiftTab.js", "/web_editor/static/lib/odoo-editor/src/commands/tab.js", "/web_editor/static/lib/odoo-editor/src/commands/toggleList.js", "/web_editor/static/src/js/wysiwyg/PeerToPeer.js", "/web_editor/static/src/js/wysiwyg/fonts.js", "/web_editor/static/src/js/base.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/color_palette.js", "/web_editor/static/src/js/wysiwyg/widgets/image_crop_widget.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/wysiwyg/widgets/media.js", "/web_editor/static/src/js/wysiwyg/widgets/media_dialog.js", "/web_editor/static/src/js/wysiwyg/widgets/upload_progress_toast.js", "/web_editor/static/src/js/wysiwyg/widgets/widgets.js", "/web_editor/static/src/js/editor/snippets.editor.js", "/web_editor/static/src/js/editor/toolbar.js", "/web_editor/static/src/js/editor/snippets.options.js", "/web_editor/static/src/js/wysiwyg/wysiwyg.js", "/web_editor/static/src/js/wysiwyg/wysiwyg_iframe.js", "/web_unsplash/static/src/js/unsplashapi.js", "/web_unsplash/static/src/js/unsplash_image_widget.js"], "mappings": "AAAA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AC/NA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;ACv+GA;;;;;;;;;AAAA;AACA;AACA;AACA;;;;ACHA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/OA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;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;ACh2CA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5JA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;;;;AC7WA;;;;;;;;;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;;;;ACxtBA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACzQA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnIA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACnIA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5CA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC5EA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC/BA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjGA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AC7nBA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;AC7KA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7WA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;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;ACvDA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC7/BA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC3PA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxmBA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5NA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;ACjTA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9dA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACxlDA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACvRA;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;AChHA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACjCA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC9uHA;;;;;;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;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC5+OA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACh0EA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AChOA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACzFA;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA", "sourcesContent": ["/*!\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", "/*!\n * jQuery Nearest plugin v1.2.1\n *\n * Finds elements closest to a single point based on screen location and pixel dimensions\n * http://gilmoreorless.github.com/jquery-nearest/\n * Open source under the MIT licence: http://gilmoreorless.mit-license.org/2011/\n *\n * Requires jQuery 1.4 or above\n * Also supports Ben Alman's \"each2\" plugin for faster looping (if available)\n */\n\n/**\n * Method signatures:\n *\n * $.nearest({x, y}, selector) - find $(selector) closest to point\n * $(elem).nearest(selector) - find $(selector) closest to elem\n * $(elemSet).nearest({x, y}) - filter $(elemSet) and return closest to point\n *\n * Also:\n * $.furthest()\n * $(elem).furthest()\n *\n * $.touching()\n * $(elem).touching()\n */\n;(function ($, undefined) {\n\n\t/**\n\t * Internal method that does the grunt work\n\t *\n\t * @param mixed selector Any valid jQuery selector providing elements to filter\n\t * @param hash options Key/value list of options for matching elements\n\t * @param mixed thisObj (optional) Any valid jQuery selector that represents self\n\t *                      for the \"includeSelf\" option\n\t * @return array List of matching elements, can be zero length\n\t */\n\tvar rPerc = /^([\\d.]+)%$/;\n\tfunction nearest(selector, options, thisObj) {\n\t\t// Normalise selector and dimensions\n\t\tselector || (selector = 'div'); // I STRONGLY recommend passing in a selector\n\t\tvar $container = $(options.container),\n\t\t\tcontainerOffset = $container.offset() || {left: 0, top: 0},\n\t\t\tcontainerDims = [\n\t\t\t\tcontainerOffset.left + $container.width(),\n\t\t\t\tcontainerOffset.top + $container.height()\n\t\t\t],\n\t\t\tpercProps = {x: 0, y: 1, w: 0, h: 1},\n\t\t\tprop, match;\n\t\tfor (prop in percProps) if (percProps.hasOwnProperty(prop)) {\n\t\t\tmatch = rPerc.exec(options[prop]);\n\t\t\tif (match) {\n\t\t\t\toptions[prop] = containerDims[percProps[prop]] * match[1] / 100;\n\t\t\t}\n\t\t}\n\n\t\t// Get elements and work out x/y points\n\t\tvar $all = $(selector),\n\t\t\tcache = [],\n\t\t\tfurthest = !!options.furthest,\n\t\t\tcheckX = !!options.checkHoriz,\n\t\t\tcheckY = !!options.checkVert,\n\t\t\tcompDist = furthest ? 0 : Infinity,\n\t\t\tpoint1x = parseFloat(options.x) || 0,\n\t\t\tpoint1y = parseFloat(options.y) || 0,\n\t\t\tpoint2x = parseFloat(point1x + options.w) || point1x,\n\t\t\tpoint2y = parseFloat(point1y + options.h) || point1y,\n\t\t\ttolerance = options.tolerance || 0,\n\t\t\thasEach2 = !!$.fn.each2,\n\t\t\t// Shortcuts to help with compression\n\t\t\tmin = Math.min,\n\t\t\tmax = Math.max;\n\n\t\t// Normalise the remaining options\n\t\tif (!options.includeSelf && thisObj) {\n\t\t\t$all = $all.not(thisObj);\n\t\t}\n\t\tif (tolerance < 0) {\n\t\t\ttolerance = 0;\n\t\t}\n\t\t// Loop through all elements and check their positions\n\t\t$all[hasEach2 ? 'each2' : 'each'](function (i, elem) {\n\t\t\tvar $this = hasEach2 ? elem : $(this),\n\t\t\t\toff = $this.offset(),\n\t\t\t\tx = off.left,\n\t\t\t\ty = off.top,\n\t\t\t\tw = $this.outerWidth(),\n\t\t\t\th = $this.outerHeight(),\n\t\t\t\tx2 = x + w,\n\t\t\t\ty2 = y + h,\n\t\t\t\tmaxX1 = max(x, point1x),\n\t\t\t\tminX2 = min(x2, point2x),\n\t\t\t\tmaxY1 = max(y, point1y),\n\t\t\t\tminY2 = min(y2, point2y),\n\t\t\t\tintersectX = minX2 >= maxX1,\n\t\t\t\tintersectY = minY2 >= maxY1,\n\t\t\t\tdistX, distY, distT, isValid;\n\t\t\tif (\n\t\t\t\t// .nearest() / .furthest()\n\t\t\t\t(checkX && checkY) ||\n\t\t\t\t// .touching()\n\t\t\t\t(!checkX && !checkY && intersectX && intersectY) ||\n\t\t\t\t// .nearest({checkVert: false})\n\t\t\t\t(checkX && intersectY) ||\n\t\t\t\t// .nearest({checkHoriz: false})\n\t\t\t\t(checkY && intersectX)\n\t\t\t) {\n\t\t\t\tdistX = intersectX ? 0 : maxX1 - minX2;\n\t\t\t\tdistY = intersectY ? 0 : maxY1 - minY2;\n\t\t\t\tdistT = intersectX || intersectY ?\n\t\t\t\t\tmax(distX, distY) :\n\t\t\t\t\tMath.sqrt(distX * distX + distY * distY);\n\t\t\t\tisValid = furthest ?\n\t\t\t\t\tdistT >= compDist - tolerance :\n\t\t\t\t\tdistT <= compDist + tolerance;\n\t\t\t\tif (isValid) {\n\t\t\t\t\tcompDist = furthest ?\n\t\t\t\t\t\tmax(compDist, distT) :\n\t\t\t\t\t\tmin(compDist, distT);\n\t\t\t\t\tcache.push({\n\t\t\t\t\t\tnode: this,\n\t\t\t\t\t\tdist: distT\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\t\t// Make sure all cached items are within tolerance range\n\t\tvar len = cache.length,\n\t\t\tfiltered = [],\n\t\t\tcompMin, compMax,\n\t\t\ti, item;\n\t\tif (len) {\n\t\t\tif (furthest) {\n\t\t\t\tcompMin = compDist - tolerance;\n\t\t\t\tcompMax = compDist;\n\t\t\t} else {\n\t\t\t\tcompMin = compDist;\n\t\t\t\tcompMax = compDist + tolerance;\n\t\t\t}\n\t\t\tfor (i = 0; i < len; i++) {\n\t\t\t\titem = cache[i];\n\t\t\t\tif (item.dist >= compMin && item.dist <= compMax) {\n\t\t\t\t\tfiltered.push(item.node);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\treturn filtered;\n\t}\n\n\t$.each(['nearest', 'furthest', 'touching'], function (i, name) {\n\n\t\t// Internal default options\n\t\t// Not exposed publicly because they're method-dependent and easily overwritten anyway\n\t\tvar defaults = {\n\t\t\tx: 0, // X position of top left corner of point/region\n\t\t\ty: 0, // Y position of top left corner of point/region\n\t\t\tw: 0, // Width of region\n\t\t\th: 0, // Height of region\n\t\t\ttolerance:   1, // Distance tolerance in pixels, mainly to handle fractional pixel rounding bugs\n\t\t\tcontainer:   document, // Container of objects for calculating %-based dimensions\n\t\t\tfurthest:    name == 'furthest', // Find max distance (true) or min distance (false)\n\t\t\tincludeSelf: false, // Include 'this' in search results (t/f) - only applies to $(elem).func(selector) syntax\n\t\t\tcheckHoriz:  name != 'touching', // Check variations in X axis (t/f)\n\t\t\tcheckVert:   name != 'touching'  // Check variations in Y axis (t/f)\n\t\t};\n\n\t\t/**\n\t\t * $.nearest() / $.furthest() / $.touching()\n\t\t *\n\t\t * Utility functions for finding elements near a specific point or region on screen\n\t\t *\n\t\t * @param hash point Co-ordinates for the point or region to measure from\n\t\t *                   \"x\" and \"y\" keys are required, \"w\" and \"h\" keys are optional\n\t\t * @param mixed selector Any valid jQuery selector that provides elements to filter\n\t\t * @param hash options (optional) Extra filtering options\n\t\t *                     Not technically needed as the options could go on the point object,\n\t\t *                     but it's good to have a consistent API\n\t\t * @return jQuery object containing matching elements in selector\n\t\t */\n\t\t$[name] = function (point, selector, options) {\n\t\t\tif (!point || point.x === undefined || point.y === undefined) {\n\t\t\t\treturn $([]);\n\t\t\t}\n\t\t\tvar opts = $.extend({}, defaults, point, options || {});\n\t\t\treturn $(nearest(selector, opts));\n\t\t};\n\n\t\t/**\n\t\t * SIGNATURE 1:\n\t\t *   $(elem).nearest(selector) / $(elem).furthest(selector) / $(elem).touching(selector)\n\t\t *\n\t\t *   Finds all elements in selector that are nearest to/furthest from elem\n\t\t *\n\t\t *   @param mixed selector Any valid jQuery selector that provides elements to filter\n\t\t *   @param hash options (optional) Extra filtering options\n\t\t *   @return jQuery object containing matching elements in selector\n\t\t *\n\t\t * SIGNATURE 2:\n\t\t *   $(elemSet).nearest(point) / $(elemSet).furthest(point) / $(elemSet).touching(point)\n\t\t *\n\t\t *   Filters elemSet to return only the elements nearest to/furthest from point\n\t\t *   Effectively a wrapper for $.nearest(point, elemSet) but with the benefits of method chaining\n\t\t *\n\t\t *   @param hash point Co-ordinates for the point or region to measure from\n\t\t *   @return jQuery object containing matching elements in elemSet\n\t\t */\n\t\t$.fn[name] = function (selector, options) {\n\t\t\tvar opts;\n\t\t\tif (selector && $.isPlainObject(selector)) {\n\t\t\t\topts = $.extend({}, defaults, selector, options || {});\n\t\t\t\treturn this.pushStack(nearest(this, opts));\n\t\t\t}\n\t\t\tvar offset = this.offset(),\n\t\t\t\tdimensions = {\n\t\t\t\t\tx: offset.left,\n\t\t\t\t\ty: offset.top,\n\t\t\t\t\tw: this.outerWidth(),\n\t\t\t\t\th: this.outerHeight()\n\t\t\t\t};\n\t\t\topts = $.extend({}, defaults, dimensions, options || {});\n\t\t\treturn this.pushStack(nearest(selector, opts, this));\n\t\t};\n\t});\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", "/** @odoo-module **/\n'use strict';\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    endPos,\n    getCursorDirection,\n    getListMode,\n    getOuid,\n    insertText,\n    isColorGradient,\n    nodeSize,\n    preserveCursor,\n    setSelection,\n    startPos,\n    toggleClass,\n    closestElement,\n    isVisible,\n    isHtmlContentSupported,\n    rgbToHex,\n    isFontAwesome,\n    getInSelection,\n    getDeepRange,\n    ancestors,\n    firstLeaf,\n    previousLeaf,\n    nextLeaf,\n    isUnremovable,\n    fillEmpty,\n    isEmptyBlock,\n    getUrlsInfosInString,\n    URL_REGEX,\n    URL_REGEX_WITH_INFOS,\n    isSelectionFormat,\n    YOUTUBE_URL_GET_VIDEO_ID,\n    unwrapContents,\n    peek,\n    rightPos,\n    isBlock,\n    isMacOS,\n    isVoidElement,\n    cleanZWS,\n    isZWS,\n    getDeepestPosition,\n    leftPos,\n    prepareUpdate,\n    splitTextNode,\n    boundariesOut,\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\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\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);\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'],\n    styles: ['text-decoration', 'font-weight', 'background-color', 'color', 'font-style', 'text-decoration-line', 'font-size']\n};\n\nconst EDITABLE_LINK_SELECTOR = 'a:not(.nav-link):not([contenteditable=\"false\"])';\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                        return closestElement(selection.anchorNode, 'P, DIV');\n                    }\n                },\n                preHistoryUndo: () => {},\n                onChange: () => {},\n                isHintBlacklisted: () => false,\n                filterMutationRecords: (records) => records,\n                onPostSanitize: () => {},\n                direction: 'ltr',\n                _t: string => string,\n                allowCommandVideo: true,\n                renderingClasses: [],\n                allowInlineAtRoot: false,\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        if (this.options.toSanitize) {\n            sanitize(editable);\n            this.options.onPostSanitize(editable);\n        }\n        this.editable = editable;\n        this.editable.classList.add(\"odoo-editor-editable\");\n        this.editable.setAttribute('dir', this.options.direction);\n\n        // Set contenteditable before clone as FF updates the content at this point.\n        this._activateContenteditable();\n\n        this._currentStep = {\n            selection: {},\n            mutations: [],\n        };\n\n        this._setLinkZws();\n\n        this._collabClientId = this.options.collaborationClientId;\n\n        // Colaborator selection and caret display.\n        this._collabSelectionInfos = new Map();\n        this._collabSelectionColor = `hsl(${(Math.random() * 360).toFixed(0)}, 75%, 50%)`;\n        this._collabSelectionsContainer = this.document.createElement('div');\n        this._collabSelectionsContainer.classList.add('oe-collaboration-selections-container');\n        this.editable.before(this._collabSelectionsContainer);\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('sanitizeElement', [editable]);\n\n        this._createCommandBar();\n\n        this.toolbarTablePicker = new TablePicker({ document: this.document });\n        this.toolbarTablePicker.addEventListener('cell-selected', ev => {\n            this.execCommand('insertTable', {\n                rowNumber: ev.detail.rowNumber,\n                colNumber: ev.detail.colNumber,\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, 'paste', this._onPaste);\n        this.addDomListener(this.editable, 'dragstart', this._onDragStart);\n        this.addDomListener(this.editable, 'drop', this._onDrop);\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, 'mousedown', this._onDoumentMousedown);\n        this.addDomListener(this.document, 'mouseup', this._onDoumentMouseup);\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.toolbar = this.options.toolbar;\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            const tablepickerDropdown = this.toolbar.querySelector('.oe-tablepicker-dropdown');\n            tablepickerDropdown && tablepickerDropdown.append(this.toolbarTablePicker.el);\n            this.toolbarTablePicker.show();\n            const tableDropdownButton = this.toolbar.querySelector('#tableDropdownButton');\n            tableDropdownButton &&\n                tableDropdownButton.addEventListener('click', () => {\n                    this.toolbarTablePicker.reset();\n                });\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            if (this.isMobile) {\n                this.editable.before(this.toolbar);\n            }\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.commandBar.destroy();\n        this.commandbarTablePicker.el.remove();\n        this._collabSelectionsContainer.remove();\n        this._resizeObserver.disconnect();\n        clearInterval(this._snapshotInterval);\n        this._pluginCall('destroy', []);\n        this.isDestroyed = true;\n    }\n\n    sanitize() {\n        this.observerFlush();\n\n        let commonAncestor, record;\n        for (record of this._currentStep.mutations) {\n            const node = this.idFind(record.parentId || record.id) || this.editable;\n            commonAncestor = commonAncestor\n                ? commonParentGet(commonAncestor, node, this.editable)\n                : node;\n        }\n        if (!commonAncestor) {\n            return false;\n        }\n\n        // sanitize and mark current position as sanitized\n        sanitize(commonAncestor);\n        this._pluginCall('sanitizeElement', [commonAncestor]);\n        this.options.onPostSanitize(commonAncestor);\n    }\n\n    addDomListener(element, eventName, callback) {\n        const boundCallback = callback.bind(this);\n        this._domListeners.push([element, eventName, boundCallback]);\n        element.addEventListener(eventName, boundCallback);\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        this.observerApply(this.filterMutationRecords(this.observer.takeRecords()));\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                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    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 targetClass = record.target.getAttribute('class');\n                    const classAfter = (targetClass && targetClass.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            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._handleCommandHint();\n        this.multiselectionRefresh();\n        this.observerActive();\n    }\n    historyGetMissingSteps({fromStepId, toStepId}) {\n        const fromIndex = this._historySteps.findIndex(x => x.id === fromStepId);\n        const toIndex = this._historySteps.findIndex(x => x.id === toStepId);\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._resetLinkZws();\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._setLinkZws();\n        this._currentStep = {\n            selection: {},\n            mutations: [],\n        };\n        this._checkStepUnbreakable = true;\n        this._recordHistorySelection();\n        this.dispatchEvent(new Event('historyStep'));\n        this.options.onChange();\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) || this.unserializeNode(record.node);\n                if (this._collabClientId) {\n                    const fakeNode = document.createElement('fake-el');\n                    fakeNode.appendChild(node);\n                    DOMPurify.sanitize(fakeNode, { IN_PLACE: true });\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                        nodeToRemove = this.unserializeNode(mutation.node);\n                        const fakeNode = document.createElement('fake-el');\n                        fakeNode.appendChild(nodeToRemove);\n                        DOMPurify.sanitize(fakeNode, { IN_PLACE: true });\n                        nodeToRemove = fakeNode.childNodes[0];\n                        if (!nodeToRemove) {\n                            continue;\n                        }\n                        this.idSet(nodeToRemove);\n                    }\n                    const next = mutation.nextId && this.idFind(mutation.nextId);\n                    const previous = !(next && next.isConnected) && mutation.previousId && this.idFind(mutation.previousId);\n                    if (next && next.isConnected) {\n                        next && next.before(nodeToRemove);\n                    } else if (previous && previous.isConnected) {\n                        previous && previous.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                    }\n                }\n            }\n        }\n        if (sideEffect) {\n            this.historySetSelection(step);\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            }\n        }\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            }\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 currentIndex;\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                    currentIndex = index;\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                    currentIndex = index;\n                    break;\n                }\n            }\n            index++;\n        }\n        currentIndex = typeof currentIndex !== 'undefined' ? currentIndex : index;\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    onExternalHistorySteps(newSteps) {\n        this.observerUnactive();\n        this._computeHistorySelection();\n\n        for (const newStep of newSteps) {\n            this._historyAddExternalStep(newStep);\n        }\n\n        this.observerActive();\n        this.historyResetLatestComputedSelection();\n        this._handleCommandHint();\n        this.multiselectionRefresh();\n    }\n\n    // Multi selection\n    // -------------------------------------------------------------------------\n\n    onExternalMultiselectionUpdate(selection) {\n        this._multiselectionDisplayClient(selection);\n        const { clientId } = selection;\n        if (this._collabSelectionInfos.has(clientId)) {\n            this._collabSelectionInfos.get(clientId).selection = selection;\n        } else {\n            this._collabSelectionInfos.set(clientId, { selection });\n        }\n    }\n\n    multiselectionRefresh() {\n        this._collabSelectionsContainer.innerHTML = '';\n        for (const { selection } of this._collabSelectionInfos.values()) {\n            this._multiselectionDisplayClient(selection);\n        }\n    }\n\n    _multiselectionDisplayClient({ selection, color, clientId, clientName = 'Anonyme' }) {\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        [anchorNode, anchorOffset] = getDeepestPosition(anchorNode, anchorOffset);\n        [focusNode, focusOffset] = getDeepestPosition(focusNode, focusOffset);\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 (e) {\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._collabSelectionsContainer.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 (clientRects.length) {\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        }\n        this._multiselectionRemoveClient(clientId);\n        this._collabSelectionsContainer.append(caretElement, ...indicators);\n    }\n\n    multiselectionRemove(clientId) {\n        this._collabSelectionInfos.delete(clientId);\n        this._multiselectionRemoveClient(clientId);\n    }\n\n    _multiselectionRemoveClient(clientId) {\n        const elements = this._collabSelectionsContainer.querySelectorAll(\n            `[data-selection-client-id=\"${clientId}\"]`,\n        );\n        for (const element of elements) {\n            element.remove();\n        }\n    }\n\n    _setLinkZws() {\n        this._resetLinkZws();\n        const selection = this.document.getSelection();\n        if (!selection.isCollapsed) {\n            return;\n        }\n        const linkInSelection = getInSelection(this.document, EDITABLE_LINK_SELECTOR);\n        const isLinkSelection = selection.anchorNode === linkInSelection;\n        let commonAncestorContainer = selection.rangeCount && selection.getRangeAt(0).commonAncestorContainer;\n        if (commonAncestorContainer) {\n            // Consider all the links in the closest block that contains the\n            // whole selection, limiting to the editable.\n            if (!this.editable.contains(commonAncestorContainer)) {\n                commonAncestorContainer = this.editable;\n            }\n            let block = closestBlock(commonAncestorContainer);\n            if (!block || !this.editable.contains(block)) {\n                block = this.editable;\n            }\n            let links = [...block.querySelectorAll(EDITABLE_LINK_SELECTOR)];\n            // Consider the links at the edges of the sibling blocks, limiting\n            // to the editable.\n            if (this.editable.contains(block)) {\n                links.push(\n                    closestElement(previousLeaf(block, this.editable, true), EDITABLE_LINK_SELECTOR),\n                    closestElement(nextLeaf(block, this.editable, true), EDITABLE_LINK_SELECTOR),\n                );\n            }\n            const offset = selection.anchorOffset;\n            let didAddZwsInLinkInSelection = false;\n            for (const link of links) {\n                if (\n                    link &&\n                    link.textContent.trim() !== '' &&\n                    // Only add the ZWS for simple (possibly styled) text links.\n                    ![link, ...link.querySelectorAll('*')].some(isBlock)\n                ) {\n                    this._insertLinkZws('start', link);\n                    // Only add the ZWS at the end if the link is in selection.\n                    if (link === linkInSelection) {\n                        this._insertLinkZws('end', link);\n                        link.classList.add('o_link_in_selection');\n                        didAddZwsInLinkInSelection = true;\n                    }\n                    const zwsAfter = this._insertLinkZws('after', link);\n                    if (!zwsAfter.parentElement || !zwsAfter.parentElement.isContentEditable) {\n                        zwsAfter.remove();\n                    }\n                }\n            }\n            if (isLinkSelection && offset && didAddZwsInLinkInSelection) {\n                // Correct the offset if the link is in selection, to account\n                // for the added ZWS.\n                setSelection(linkInSelection, Math.min(offset + 1, linkInSelection.childNodes.length));\n            }\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 sel = this.document.getSelection();\n                if (sel.anchorNode && ancestors(sel.anchorNode).includes(this.editable)) {\n                    this.execCommand(buttonEl.dataset.call, buttonEl.dataset.arg1);\n\n                    ev.preventDefault();\n                    this._updateToolbar();\n                }\n            });\n        }\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        let range = getDeepRange(this.editable, {\n            sel,\n            splitText: true,\n            select: true,\n            correctTripleClick: true,\n        });\n        if (!range) return;\n        let { startContainer: start, startOffset, endContainer: end, endOffset } = range;\n        const doJoin =\n            (closestBlock(start) !== closestBlock(range.commonAncestorContainer) ||\n            closestBlock(end) !== closestBlock(range.commonAncestorContainer))\n            && (closestBlock(start).tagName !== 'TD' && closestBlock(end).tagName !== 'TD');\n        let next = nextLeaf(end, this.editable);\n        const splitEndTd = closestElement(end, 'td') && end.nextSibling;\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        // Let the DOM split and delete the range.\n        const contents = range.extractContents();\n\n        setSelection(start, nodeSize(start));\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        // Restore table contents removed by extractContents.\n        const tds = [...contents.querySelectorAll('td')].filter(n => !closestElement(n, 'table'));\n        let currentFragmentTr, currentTr;\n        const currentTd = closestElement(range.endContainer, 'td');\n        tds.forEach((td, i) => {\n            const parentFragmentTr = closestElement(td, 'tr');\n            // Skip the first and the last partially selected TD.\n            if (i && !(splitEndTd && i === tds.length - 1)) {\n                if (parentFragmentTr && parentFragmentTr !== currentFragmentTr && currentTr && [...parentFragmentTr.querySelectorAll('td')].every(td => tds.includes(td))) {\n                    currentTr.after(parentFragmentTr);\n                    currentTr = parentFragmentTr;\n                    parentFragmentTr.textContent = '';\n                } else {\n                    if (parentFragmentTr !== currentFragmentTr) {\n                        currentTr = currentTr\n                            ? currentTr.nextElementSibling\n                            : closestElement(range.endContainer, 'tr').nextElementSibling;\n                    }\n                    currentTr ? currentTr.prepend(td) : currentTd.after(td);\n                    td.textContent = '';\n                }\n            }\n            currentFragmentTr = parentFragmentTr;\n        });\n        this.observerFlush();\n        this._toRollback = false; // Errors caught with observerFlush were already handled.\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, noBlocks = true) =>\n            !isVisible(node, noBlocks) && !isUnremovable(node);\n        const endIsStart = end === start;\n        while (end && isRemovableInvisible(end, false) && !end.contains(range.endContainer)) {\n            const parent = end.parentNode;\n            end.remove();\n            end = parent;\n        }\n        // Same with the start container\n        const table = closestElement(start, 'table');\n        // For selection starting at the beginning of the table and ending outside the\n        // table, the block should not be considered visible in order to remove the table.\n        const noBlocks = (table && firstLeaf(table) === start && !table.contains(end)) ? false : true;\n        while (\n            start &&\n            isRemovableInvisible(start, noBlocks) &&\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            fillEmpty(closestBlock(start));\n        }\n        fillEmpty(closestBlock(range.endContainer));\n        const joinWith = range.endContainer;\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) &&\n            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 (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     * 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 = computedStyle.backgroundColor;\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 (\n            !this.editable.contains(sel.anchorNode) ||\n            (sel.anchorNode !== sel.focusNode && !this.editable.contains(sel.focusNode))\n        ) {\n            // Do not apply commands out of the editable area.\n            return false;\n        }\n        this._resetLinkZws();\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                range.cloneContents().textContent === '\\u200B'\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        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    _resetLinkZws(element = this.editable) {\n        this.observerUnactive('_resetLinkZws');\n        element.querySelectorAll('[data-o-link-zws]').forEach(zws => zws.remove());\n        element.querySelectorAll('.o_link_in_selection').forEach(link => link.classList.remove('o_link_in_selection'));\n        this.observerActive('_resetLinkZws');\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 (isVoidElement(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    _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    // HISTORY\n    // =======\n\n    /**\n     * @private\n     * @returns {Object}\n     */\n    _computeHistorySelection() {\n        const sel = this.document.getSelection();\n        if (!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 (!sel.isCollapsed && 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 && (parentTextContent === '' || parentTextContent === '\\u200B')\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\n    // COMMAND BAR\n    // ===========\n\n    _createCommandBar() {\n        this.commandbarTablePicker = new TablePicker({\n            document: this.document,\n            floating: true,\n            getContextFromParentRect: this.options.getContextFromParentRect,\n        });\n\n        document.body.appendChild(this.commandbarTablePicker.el);\n\n        this.commandbarTablePicker.addEventListener('cell-selected', ev => {\n            this.execCommand('insertTable', {\n                rowNumber: ev.detail.rowNumber,\n                colNumber: ev.detail.colNumber,\n            });\n        });\n\n        const mainCommands = [\n            {\n                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Heading 1'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Heading 2'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Heading 3'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Text'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Bulleted list'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Numbered list'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Checklist'),\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                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Table'),\n                description: this.options._t('Insert a table.'),\n                fontawesome: 'fa-table',\n                isDisabled: () => !this.isSelectionInBlockRoot(),\n                callback: () => {\n                    this.commandbarTablePicker.show();\n                },\n            },\n            {\n                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Switch direction'),\n                description: this.options._t('Switch the text\\'s direction.'),\n                fontawesome: 'fa-exchange',\n                callback: () => {\n                    this.execCommand('switchDirection');\n                },\n            },\n        ];\n        if (this.options.commands && !this.options.commands.find(c =>  c.title === this.options._t('Separator'))) {\n            mainCommands.push({\n                groupName: this.options._t('Basic blocks'),\n                title: this.options._t('Separator'),\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        this.commandBar = new Powerbox({\n            editable: this.editable,\n            document: this.document,\n            getContextFromParentRect: this.options.getContextFromParentRect,\n            _t: this.options._t,\n            onShow: () => {\n                this.commandbarTablePicker.hide();\n            },\n            shouldActivate: () => !!this.options.getPowerboxElement(),\n            onActivate: () => {\n                this._beforeCommandbarStepIndex = this._historySteps.length - 1;\n            },\n            preValidate: () => {\n                this.historyRevertUntil(this._beforeCommandbarStepIndex);\n                this.historyStep(true);\n                this._historyStepsStates.set(peek(this._historySteps).id, 'consumed');\n                this.editable.focus();\n                getDeepRange(this.editable, { select: true });\n            },\n            postValidate: () => {\n                this.historyStep(true);\n            },\n            commands: [...mainCommands, ...(this.options.commands || [])],\n        });\n    }\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.options.toolbar) return;\n        if (!this.options.autohideToolbar && this.toolbar.style.visibility !== 'visible') {\n            this.toolbar.style.visibility = 'visible';\n        }\n\n        const sel = this.document.getSelection();\n        if (!sel.anchorNode) {\n            show = false;\n        }\n        if (this.options.autohideToolbar && !this.toolbar.contains(sel.anchorNode)) {\n            if (!this.isMobile) {\n                if (show !== undefined) {\n                    this.toolbar.style.visibility = show ? 'visible' : 'hidden';\n                }\n                if (show === false) {\n                    return;\n                }\n            }\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            const closestStartContainer = closestElement(sel.getRangeAt(0).startContainer, '*');\n            const selectionStartStyle = getComputedStyle(closestStartContainer);\n\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 fontSizeValue = this.toolbar.querySelector('#fontSizeCurrentValue');\n            if (fontSizeValue) {\n                fontSizeValue.textContent = /\\d+/.exec(selectionStartStyle.fontSize).pop();\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        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, tag, isList] of [\n            ['paragraph', 'P', false],\n            ['pre', 'PRE', false],\n            ['heading1', 'H1', false],\n            ['heading2', 'H2', false],\n            ['heading3', 'H3', false],\n            ['heading4', 'H4', false],\n            ['heading5', 'H5', false],\n            ['heading6', 'H6', false],\n            ['blockquote', 'BLOCKQUOTE', 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) === tag\n                    : block.tagName === tag;\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 linkNode = getInSelection(this.document, 'a');\n        const linkButton = this.toolbar.querySelector('#createLink');\n        linkButton && linkButton.classList.toggle('active', !!linkNode);\n        const unlinkButton = this.toolbar.querySelector('#unlink');\n        unlinkButton && unlinkButton.classList.toggle('d-none', !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        if (this.options.autohideToolbar && !this.isMobile && !this.toolbar.contains(sel.anchorNode)) {\n            this._positionToolbar();\n        }\n    }\n    updateToolbarPosition() {\n        if (\n            this.options.autohideToolbar &&\n            !this.isMobile &&\n            getComputedStyle(this.toolbar).visibility === 'visible'\n        ) {\n            this._positionToolbar();\n        }\n    }\n    _positionToolbar() {\n        const OFFSET = 10;\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        const correctedSelectionRect = isSelectionPotentiallyBugged && startRect ? startRect : selRect;\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\n        // Get left position.\n        let left = correctedSelectionRect.left + OFFSET;\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        // 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\n        // Calculate toolbar dimensions including the arrow.\n        const toolbarTop = Math.min(top , top + arrowTopPos);\n        const toolbarBottom = Math.max(top + toolbarHeight, top + arrowTopPos + 20);\n\n        // Hide toolbar if it overflows the scroll container.\n        const distToScrollContainer = Math.min(toolbarTop - scrollContainerRect.top,\n                                                scrollContainerRect.bottom - toolbarBottom);\n        this.toolbar.classList.toggle('d-none', distToScrollContainer < OFFSET / 2);\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 {string}\n     */\n    _prepareClipboardData(clipboardData) {\n        const container = document.createElement('fake-container');\n        container.innerHTML = clipboardData;\n\n        for (const tableElement of container.querySelectorAll('table')) {\n            tableElement.classList.add('table', 'table-bordered');\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 temporaryContainer = document.createElement('template');\n        let temporaryP = document.createElement('p');\n        for (const child of [...container.childNodes]) {\n            if (isBlock(child)) {\n                if (temporaryP.childNodes.length > 0) {\n                    temporaryContainer.content.appendChild(temporaryP);\n                    temporaryP = document.createElement('p');\n                }\n                temporaryContainer.content.appendChild(child);\n            } else {\n                temporaryP.appendChild(child);\n            }\n\n            if (temporaryP.childNodes.length > 0) {\n                temporaryContainer.content.appendChild(temporaryP);\n            }\n        }\n        return temporaryContainer.innerHTML;\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                // Unwrap the illegal node's contents.\n                for (const unwrappedNode of unwrapContents(node)) {\n                    this._cleanForPaste(unwrappedNode);\n                }\n            }\n        } else if (node.nodeType !== Node.TEXT_NODE) {\n            if (node.nodeName === 'TD' && node.hasAttribute('style')) {\n                // TD tags do not support style in v15, move style to children\n                // or new SPAN.\n                const span = node.childNodes.length === 1 && node.firstChild.nodeName === 'SPAN'\n                    ? node.firstChild\n                    : this.document.createElement('SPAN');\n                // Background-color on TD (cell) != on span (text).\n                // Prevent it from being copied from cell to text.\n                node.style.backgroundColor = null;\n                for (const styleText of node.getAttribute('style').split(';')) {\n                    // Give parent's style to child, unless already set.\n                    const [styleName, styleValue] = styleText.split(':').map(x => x.trim());\n                    if (!span.style[styleName]) {\n                        span.style[styleName] = styleValue;\n                    }\n                }\n                if (span.parentNode !== node) {\n                    // A new span was needed to apply the styles. \n                    // Use it to wrap the nodes in the cell.\n                    span.append(...node.childNodes);\n                    node.append(span);\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            }\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                    const spanInlineStyles = attribute.value.split(';').map(x => x.trim());\n                    const allowedSpanInlineStyles = spanInlineStyles.filter(rawStyle => {\n                        return CLIPBOARD_WHITELISTS.styles.includes(rawStyle.split(':')[0].trim());\n                    });\n                    node.removeAttribute(attribute.name);\n                    if (allowedSpanInlineStyles.length > 0) {\n                        node.setAttribute(attribute.name, allowedSpanInlineStyles.join(';'));\n                    } else 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 instanceof Attr) {\n            return CLIPBOARD_WHITELISTS.attributes.includes(item.name);\n        } else if (typeof item === 'string') {\n            return CLIPBOARD_WHITELISTS.classes.some(okClass =>\n                okClass instanceof RegExp ? okClass.test(item) : okClass === item,\n            );\n        } else {\n            const allowedSpanStyles = CLIPBOARD_WHITELISTS.styles.map(s => `span[style*=\"${s}\"]`);\n            return (\n                item.nodeType === Node.TEXT_NODE ||\n                (\n                    item.matches &&\n                    item.matches([...CLIPBOARD_WHITELISTS.nodes, ...allowedSpanStyles].join(','))\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 parent = node.parentNode;\n        const next = node.nextSibling;\n        this.observerFlush();\n        node.remove();\n        this.observer.takeRecords();\n        node.setAttribute(attributeName, attributeValue);\n        this.observerFlush();\n        DOMPurify.sanitize(node, { IN_PLACE: true });\n        if (next) {\n            next.before(node);\n        } else if (parent) {\n            parent.append(node);\n        }\n        this.observer.takeRecords();\n    }\n    _insertLinkZws(side, link) {\n        this.observerUnactive('_insertLinkZws');\n        const span = document.createElement('span');\n        span.setAttribute('data-o-link-zws', side);\n        if (side !== 'end') {\n            span.setAttribute('contenteditable', 'false');\n        }\n        span.textContent = '\\u200B';\n        if (side === 'start') {\n            link.prepend(span);\n        } else if (side === 'end') {\n            link.append(span);\n        } else if (side === 'after') {\n            link.after(span);\n        }\n        this.observerActive('_insertLinkZws');\n        return span;\n    }\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onBeforeInput(ev) {\n        this._lastBeforeInputType = ev.inputType;\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        // 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\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        this._resetLinkZws();\n        if (this.keyboardType === KEYBOARD_TYPES.PHYSICAL || !wasCollapsed) {\n            if (ev.inputType === 'deleteContentBackward') {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                this._applyCommand('oDeleteBackward');\n            } else if (ev.inputType === 'deleteContentForward' || isChromeDeleteforward) {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                this._applyCommand('oDeleteForward');\n            } else if (ev.inputType === 'insertParagraph' || isChromeInsertParagraph) {\n                this._compositionStep();\n                this.historyRollback();\n                ev.preventDefault();\n                if (this._applyCommand('oEnter') === UNBREAKABLE_ROLLBACK_CODE) {\n                    const brs = this._applyRawCommand('oShiftEnter');\n                    const anchor = brs[0].parentElement;\n                    if (anchor.nodeName === 'A') {\n                        if (brs.includes(anchor.firstChild)) {\n                            brs.forEach(br => anchor.before(br));\n                            setSelection(...rightPos(brs[brs.length - 1]));\n                        } else if (brs.includes(anchor.lastChild)) {\n                            brs.forEach(br => anchor.after(br));\n                            setSelection(...rightPos(brs[0]));\n                        }\n                    }\n                    this.historyStep();\n                }\n            } else if (['insertText', 'insertCompositionText'].includes(ev.inputType)) {\n                // insertCompositionText, courtesy of Samsung keyboard.\n                const selection = this.document.getSelection();\n                // Detect that text was selected and change behavior only if it is the case,\n                // since it is the only text insertion case that may cause problems.\n                const wasTextSelected = anchorNodeOid !== focusNodeOid || anchorOffset !== focusOffset;\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                if (wasTextSelected || isUnitTests || latestSelectionInsideEmptyTag) {\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._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                // Check for url after user insert a space so we won't transform an incomplete url.\n                if (\n                    ev.data &&\n                    ev.data === ' ' &&\n                    selection &&\n                    selection.anchorNode &&\n                    !closestElement(selection.anchorNode).closest('a') &&\n                    selection.anchorNode.nodeType === Node.TEXT_NODE &&\n                    (!this.commandBar._active ||\n                        this.commandBar._currentOpenOptions.closeOnSpace !== true)\n                ) {\n                    const textSliced = selection.anchorNode.textContent.slice(0, selection.anchorOffset);\n                    const textNodeSplitted = textSliced.split(/\\s/);\n\n                    // Remove added space\n                    textNodeSplitted.pop();\n                    const potentialUrl = textNodeSplitted.pop();\n                    const lastWordMatch = potentialUrl.match(URL_REGEX_WITH_INFOS);\n\n                    if (lastWordMatch) {\n                        const matches = getUrlsInfosInString(textSliced);\n                        const match = matches[matches.length - 1];\n                        const cloneRange = selection.getRangeAt(0).cloneRange();\n                        const range = this.document.createRange();\n                        range.setStart(selection.anchorNode, match.index);\n                        range.setEnd(selection.anchorNode, match.index + match.length);\n                        const link = this._createLink(range.extractContents().textContent, match.url);\n                        range.insertNode(link);\n                        // Inserting an element into a range clears the selection in Safari\n                        // Hence, use the cloned range to reselect it.\n                        selection.removeAllRanges();\n                        selection.addRange(cloneRange);\n                    }\n                    selection.collapseToEnd();\n                }\n                this.historyStep();\n            } else {\n                this.historyStep();\n            }\n        } else if (ev.inputType === 'insertCompositionText') {\n            this._fromCompositionText = true;\n        }\n    }\n\n    /**\n     * @private\n     */\n    _onKeyDown(ev) {\n        this.keyboardType =\n            ev.key === 'Unidentified' ? KEYBOARD_TYPES.VIRTUAL : KEYBOARD_TYPES.PHYSICAL;\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) {\n                this._resetLinkZws();\n                this.deleteRange(selection);\n                this._setLinkZws();\n            }\n        }\n        if (ev.key === 'Backspace' && !ev.ctrlKey && !ev.metaKey) {\n            // backspace\n            // We need to hijack it because firefox doesn't trigger a\n            // deleteBackward input event with a collapsed selection in front of\n            // a contentEditable=\"false\" (eg: font awesome).\n            const selection = this.document.getSelection();\n            if (selection.isCollapsed) {\n                ev.preventDefault();\n                this._applyCommand('oDeleteBackward');\n            }\n        } else if (ev.key === 'Tab') {\n            // Tab\n            const sel = this.document.getSelection();\n            const closestTag = (closestElement(sel.anchorNode, 'li, table', true) || {}).tagName;\n\n            if (closestTag === 'LI') {\n                this._applyCommand('indentList', ev.shiftKey ? 'outdent' : 'indent');\n            } else if (closestTag === 'TABLE') {\n                this._onTabulationInTable(ev);\n            } else if (!ev.shiftKey) {\n                this.execCommand('insertText', '\\u00A0 \\u00A0\\u00A0');\n            }\n            ev.preventDefault();\n            ev.stopPropagation();\n        } else if (ev.shiftKey && ev.key === \"Enter\") {\n            ev.preventDefault();\n            this._applyCommand('oShiftEnter');\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        } else if (IS_KEYBOARD_EVENT_ITALIC(ev)) {\n            // Ctrl-I\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('italic');\n        } else if (IS_KEYBOARD_EVENT_UNDERLINE(ev)) {\n            // Ctrl-U\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('underline');\n        } else if (IS_KEYBOARD_EVENT_STRIKETHROUGH(ev)) {\n            // Ctrl-5 / Ctrl-shift-(\n            ev.preventDefault();\n            ev.stopPropagation();\n            this.execCommand('strikeThrough');\n        } else if (IS_KEYBOARD_EVENT_LEFT_ARROW(ev)) {\n            if (ev.shiftKey) {\n                this._resetLinkZws();\n            }\n            getDeepRange(this.editable, { select: true });\n            let { anchorNode, anchorOffset, focusNode, focusOffset } = this.document.getSelection();\n            // Find previous character.\n            let previousCharacter = focusOffset > 0 && focusNode.textContent[focusOffset - 1];\n            if (!previousCharacter) {\n                focusNode = previousLeaf(focusNode);\n                focusOffset = nodeSize(focusNode);\n                previousCharacter = focusNode.textContent[focusOffset - 1];\n            }\n            // Move selection if previous character is zero-width space\n            if (previousCharacter === '\\u200B' && !focusNode.parentElement.hasAttribute('data-o-link-zws')) {\n                focusOffset -= 1;\n                while (focusNode && (focusOffset < 0 || !focusNode.textContent[focusOffset])) {\n                    focusNode = nextLeaf(focusNode);\n                    focusOffset = focusNode && nodeSize(focusNode);\n                }\n                const startContainer = ev.shiftKey ? anchorNode : focusNode;\n                const startOffset = ev.shiftKey ? anchorOffset : focusOffset;\n                setSelection(startContainer, startOffset, focusNode, focusOffset);\n            }\n        } else if (IS_KEYBOARD_EVENT_RIGHT_ARROW(ev)) {\n            if (ev.shiftKey) {\n                this._resetLinkZws();\n            }\n            getDeepRange(this.editable, { select: true });\n            let { anchorNode, anchorOffset, focusNode, focusOffset } = this.document.getSelection();\n            // Find next character.\n            let nextCharacter = focusNode.textContent[focusOffset];\n            if (!nextCharacter) {\n                focusNode = nextLeaf(focusNode);\n                focusOffset = 0;\n                nextCharacter = focusNode.textContent[focusOffset];\n            }\n            // Move selection if next character is zero-width space\n            if (nextCharacter === '\\u200B' && !focusNode.parentElement.hasAttribute('data-o-link-zws')) {\n                focusOffset += 1;\n                while (focusNode && !focusNode.textContent[focusOffset]) {\n                    focusNode = nextLeaf(focusNode);\n                    focusOffset = 0;\n                }\n                const startContainer = ev.shiftKey ? anchorNode : focusNode;\n                const startOffset = ev.shiftKey ? anchorOffset : focusOffset;\n                setSelection(startContainer, startOffset, focusNode, focusOffset);\n            }\n        }\n    }\n    /**\n     * @private\n     */\n    _onSelectionChange() {\n        const selection = this.document.getSelection();\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 (selection.anchorNode === this.editable && selection.focusNode === this.editable) {\n            getDeepRange(\n                this.editable,\n                {\n                    correctTripleClick: true,\n                    select: true,\n                });\n            // The selection is changed in `getDeepRange` and will therefore\n            // re-trigger the _onSelectionChange.\n            return;\n        }\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        this._updateToolbar(!selection.isCollapsed && this.isSelectionInEditable(selection));\n\n        if (this._currentMouseState === 'mouseup') {\n            this._fixFontAwesomeSelection();\n        }\n        if (selection.rangeCount && selection.getRangeAt(0)) {\n            // Handle selection/navigation at the edges of links.\n            const link = getInSelection(this.document, EDITABLE_LINK_SELECTOR);\n            if (link && selection.isCollapsed) {\n                // 1. If the selection starts or ends at the end of a link\n                //    (after the end zws), move the selection after the \"after\"\n                //    zws. This ensures that the cursor is visibly outside the\n                //    link. We want to do this only if the link has an end zws\n                //    to prevent ejecting the selection when moving in from the\n                //    right.\n                const endZws = link.querySelector('[data-o-link-zws=\"end\"]');\n                const isAtEndOfLink = (\n                    // The selection is at the end of the link, ie. at offset\n                    // max of the link, with no next leaf that is in the link.\n                    endZws && selection.anchorOffset === nodeSize(selection.anchorNode) &&\n                    closestElement(selection.anchorNode, EDITABLE_LINK_SELECTOR) === link &&\n                    closestElement(nextLeaf(selection.anchorNode, this.editable), EDITABLE_LINK_SELECTOR) !== link\n                );\n                if (isAtEndOfLink) {\n                    let afterZws = link.nextElementSibling;\n                    if (!afterZws) {\n                        afterZws = this._insertLinkZws('after', link);\n                    }\n                    setSelection(\n                        afterZws.nextSibling || afterZws.parentElement,\n                        afterZws.nextSibling ? 0 : nodeSize(afterZws.parentElement),\n                    );\n                    return; // The selection is changed and will therefore re-trigger the _onSelectionChange.\n                }\n            }\n            // 2. Make sure the link has the required zws if the selection\n            //    wasn't changed.\n            this._setLinkZws();\n\n            if (this.options.onCollaborativeSelectionChange) {\n                this.options.onCollaborativeSelectionChange(this.getCurrentCollaborativeSelection());\n            }\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        return selection && selection.anchorNode &&\n            closestElement(selection.anchorNode).isContentEditable && closestElement(selection.focusNode).isContentEditable &&\n            this.editable.contains(selection.anchorNode) && this.editable.contains(selection.focusNode);\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    /**\n     * Returns true if the current selection content is only one ZWS\n     *\n     * @private\n     * @param {Object} selection\n     * @returns {boolean}\n     */\n    _isSelectionOnlyZws(selection) {\n        let range = selection.getRangeAt(0);\n        if (selection.isCollapsed || !range) {\n            return false;\n        }\n        return range.cloneContents().textContent === '\\u200B';\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        };\n    }\n\n    clean() {\n        this.observerUnactive();\n        for (const hint of this.editable.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.cleanForSave();\n        this.observerActive();\n    }\n\n    /**\n     * initialise the provided element to be ready for edition\n     *\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('oe-keep-contenteditable', '');\n        }\n    }\n\n    cleanForSave(element = this.editable) {\n        this._pluginCall('cleanForSave', [element]);\n        // Remove all link ZWS.\n        this._resetLinkZws(element);\n        // Clean the remaining ZeroWidthspaces added by the `fillEmpty` function\n        // ( contain \"oe-zws-empty-inline\" attr)\n        // If the element contain more than just a ZWS,\n        // we remove it and clean the attribute.\n        // If the element have a class,\n        // we only remove the attribute to ensure we don't break some style.\n        // Otherwise we remove the entire inline element.\n        for (const emptyElement of element.querySelectorAll('[oe-zws-empty-inline]')) {\n            if (isZWS(emptyElement)) {\n                if (emptyElement.classList.length > 0) {\n                    emptyElement.removeAttribute('oe-zws-empty-inline');\n                } else {\n                    emptyElement.remove();\n                }\n            } else {\n                cleanZWS(emptyElement);\n                emptyElement.removeAttribute('oe-zws-empty-inline');\n            }\n        }\n        sanitize(element);\n\n        // Remove contenteditable=false on elements\n        for (const el of element.querySelectorAll('[contenteditable=\"false\"]')) {\n            if (!el.hasAttribute('oe-keep-contenteditable')) {\n                el.removeAttribute('contenteditable');\n            }\n        }\n        // Remove oe-keep-contenteditable on elements\n        for (const el of element.querySelectorAll('[oe-keep-contenteditable]')) {\n            el.removeAttribute('oe-keep-contenteditable');\n        }\n\n        // Remove Zero Width Spaces on Font awesome elements\n        const faSelector = 'i.fa,span.fa,i.fab,span.fab,i.fad,span.fad,i.far,span.far';\n        for (const el of element.querySelectorAll(faSelector)) {\n            cleanZWS(el);\n        }\n\n    }\n    /**\n     * Handle the hint preview for the commandbar.\n     * @private\n     */\n    _handleCommandHint() {\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            'UL LI': this.options._t('List'),\n            'OL LI': this.options._t('List'),\n            'CL LI': this.options._t('To-do'),\n        };\n\n        for (const hint of this.editable.querySelectorAll('.oe-hint')) {\n            if (hint.classList.contains('oe-command-temporary-hint') || !isEmptyBlock(hint)) {\n                this.observerUnactive();\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                this.observerActive();\n            }\n        }\n\n        if (this.options.showEmptyElementHint) {\n            for (const [selector, text] of Object.entries(selectors)) {\n                for (const el of this.editable.querySelectorAll(selector)) {\n                    if (!this.options.isHintBlacklisted(el)) {\n                        this._makeHint(el, text);\n                    }\n                }\n            }\n        }\n\n        const block = this.options.getPowerboxElement();\n        if (block) {\n            this._makeHint(block, this.options._t('Type \"/\" for commands'), true);\n        }\n\n        // placeholder hint\n        const sel = this.document.getSelection();\n        if (this.editable.textContent.trim() === '' && this.options.placeholder && !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            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    _onMouseup(ev) {\n        this._currentMouseState = ev.type;\n\n        this._fixFontAwesomeSelection();\n    }\n\n    _onMouseDown(ev) {\n        this._currentMouseState = ev.type;\n\n        this._activateContenteditable();\n\n        // Ignore any changes that might have happened before this point.\n        this.observer.takeRecords();\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                ev.preventDefault();\n                this.historyStep();\n            }\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    _onDoumentMousedown(event) {\n        if (this.toolbar && !ancestors(event.target, this.editable).includes(this.toolbar)) {\n            this.toolbar.style.pointerEvents = 'none';\n        }\n    }\n\n    _onDoumentMouseup() {\n        if (this.toolbar) {\n            this.toolbar.style.pointerEvents = 'auto';\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.style.width = '100%';\n            imageNode.classList.add('img-fluid');\n            imageNode.dataset.fileName = imageFile.name;\n            promises.push(getImageUrl(imageFile).then(url => {\n                imageNode.src = url;\n                return imageNode.outerHTML;\n            }));\n        }\n        return Promise.all(promises).then(html => html.join(''));\n    }\n    /**\n     * Handle safe pasting of html or plain text into the editor.\n     */\n    _onPaste(ev) {\n        ev.preventDefault();\n        const sel = this.document.getSelection();\n        const files = getImageFiles(ev.clipboardData);\n        const targetSupportsHtmlContent = isHtmlContentSupported(sel.anchorNode);\n        const clipboardHtml = ev.clipboardData.getData('text/html');\n        // Replace entire link if its label is fully selected.\n        const link = closestElement(sel.anchorNode, 'a', true);\n        if (link && sel.toString().replace(/\\u200B/g, '') === link.innerText.replace(/\\u200B/g, '')) {\n            const start = leftPos(link);\n            link.remove();\n            setSelection(...start, ...start, false);\n        }\n        if ((files.length || clipboardHtml) && targetSupportsHtmlContent) {\n            // Differentiate or choose between images and html\n            const clipboardElem = document.createElement('template');\n            clipboardElem.innerHTML = this._prepareClipboardData(clipboardHtml);\n            if (files.length && !clipboardElem.content.querySelector('table')) {\n                this.addImagesFiles(files).then(html => this._applyCommand('insertHTML', this._prepareClipboardData(html)));\n            } else {\n                if(closestElement(sel.anchorNode, 'a')) {\n                    this._applyCommand('insertText', clipboardElem.content.textContent);\n                }\n                else {\n                    this._applyCommand('insertHTML', clipboardElem.content);\n                }\n            }\n        } else {\n            const text = ev.clipboardData.getData('text/plain');\n            const splitAroundUrl = text.split(URL_REGEX);\n            const selectionIsInsideALink = !!closestElement(sel.anchorNode, 'a');\n            if (splitAroundUrl.length === 3 && !splitAroundUrl[0] && !splitAroundUrl[2]) {\n                // Pasted content is a single URL.\n                const url = /^https?:\\/\\//i.test(text) ? text : 'https://' + 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'].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('insertHTML', img);\n                    } else {\n                        this._applyCommand('insertText', text);\n                    }\n                } else if (isImageUrl || youtubeUrl) {\n                    // Open powerbox with commands to embed media or paste as link.\n                    // Insert URL as text, store history step index to revert it later.\n                    const stepIndexBeforeInsert = this._historySteps.length - 1;\n                    this._applyCommand('insertText', text);\n                    const revertTextInsertion = () => {\n                        this.historyRevertUntil(stepIndexBeforeInsert);\n                        this.historyStep(true);\n                        this._historyStepsStates.set(peek(this._historySteps).id, 'consumed');\n                    };\n                    let commands;\n                    const pasteAsURLCommand = {\n                        title: this.options._t('Paste as URL'),\n                        description: this.options._t('Create an URL.'),\n                        fontawesome: 'fa-link',\n                        shouldPreValidate: () => false,\n                        callback: () => {\n                            revertTextInsertion();\n                            this._applyRawCommand('insertHTML', this._createLink(text, url))\n                        },\n                    };\n                    if (isImageUrl) {\n                        const embedImageCommand = {\n                            title: this.options._t('Embed Image'),\n                            description: this.options._t('Embed the image in the document.'),\n                            fontawesome: 'fa-image',\n                            shouldPreValidate: () => false,\n                            callback: () => {\n                                revertTextInsertion();\n                                const img = document.createElement('IMG');\n                                img.setAttribute('src', url);\n                                this._applyRawCommand('insertHTML', img);\n                            },\n                        };\n                        commands = [embedImageCommand, pasteAsURLCommand];\n                    } else {\n                         // URL is a YouTube video.\n                        const embedVideoCommand = {\n                            title: this.options._t('Embed Youtube Video'),\n                            description: this.options._t('Embed the youtube video in the document.'),\n                            fontawesome: 'fa-youtube-play',\n                            shouldPreValidate: () => false,\n                            callback: () => {\n                                revertTextInsertion();\n                                let videoElement;\n                                if (this.options.getYoutubeVideoElement) {\n                                    videoElement = 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('insertHTML', videoElement);\n                            },\n                        };\n                        commands = [embedVideoCommand, pasteAsURLCommand];\n                    }\n                    this.commandBar.open({ commands, forceShow: true });\n                } else {\n                    this._applyCommand('insertHTML', 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                        : 'https://' + splitAroundUrl[i];\n                    // Even indexes will always be plain text, and odd indexes will always be URL.\n                    // only allow images emebed inside an existing link. No other url or video embed.\n                    if (i % 2 && !selectionIsInsideALink) {\n                        this._applyCommand('insertHTML', 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('insertText', modifiedTextFragment);\n                            if (textIndex < textFragments.length) {\n                                this._applyCommand('oShiftEnter');\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('insertHTML', this._prepareClipboardData(html));\n        } else if (fileTransferItems.length) {\n            this.addImagesFiles(fileTransferItems).then(html => {\n                this.execCommand('insertHTML', this._prepareClipboardData(html));\n            });\n        } else if (htmlTransferItem) {\n            htmlTransferItem.getAsString(pastedText => {\n                this.execCommand('insertHTML', 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            setSelection(...startPos(cursorDestination), ...endPos(cursorDestination), true);\n        } else if (direction === DIRECTIONS.RIGHT) {\n            this.execCommand('addRowBelow');\n            this._onTabulationInTable(ev);\n        }\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 (isFontAwesome(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 (isFontAwesome(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';\n", "/** @odoo-module **/\nimport {\n    closestBlock,\n    endPos,\n    fillEmpty,\n    getListMode,\n    isBlock,\n    isEmptyBlock,\n    isVisibleEmpty,\n    moveNodes,\n    preserveCursor,\n    isFontAwesome,\n    getDeepRange,\n    isUnbreakable,\n    closestElement,\n    getUrlsInfosInString,\n    isVoidElement,\n    unwrapContents,\n} from './utils.js';\n\nconst NOT_A_NUMBER = /[^\\d]/g;\nexport function areSimilarElements(node, node2) {\n    if (\n        !node ||\n        !node2 ||\n        node.nodeType !== Node.ELEMENT_NODE ||\n        node2.nodeType !== Node.ELEMENT_NODE\n    ) {\n        return false;\n    }\n    if (node.tagName !== node2.tagName) {\n        return false;\n    }\n    for (const att of node.attributes) {\n        const att2 = node2.attributes[att.name];\n        if ((att2 && att2.value) !== att.value) {\n            return false;\n        }\n    }\n    for (const att of node2.attributes) {\n        const att2 = node.attributes[att.name];\n        if ((att2 && att2.value) !== att.value) {\n            return false;\n        }\n    }\n    function isNotNoneValue(value) {\n        return value && value !== 'none';\n    }\n    if (\n        isNotNoneValue(getComputedStyle(node, ':before').getPropertyValue('content')) ||\n        isNotNoneValue(getComputedStyle(node, ':after').getPropertyValue('content')) ||\n        isNotNoneValue(getComputedStyle(node2, ':before').getPropertyValue('content')) ||\n        isNotNoneValue(getComputedStyle(node2, ':after').getPropertyValue('content'))\n    ) {\n        return false;\n    }\n    if (node.tagName === 'LI' && node.classList.contains('oe-nested')) {\n        return (\n            node.lastElementChild &&\n            node2.firstElementChild &&\n            getListMode(node.lastElementChild) === getListMode(node2.firstElementChild)\n        );\n    }\n    if (['UL', 'OL'].includes(node.tagName)) {\n        return !isVisibleEmpty(node) && !isVisibleEmpty(node2);\n    }\n    if (isBlock(node) || isVisibleEmpty(node) || isVisibleEmpty(node2)) {\n        return false;\n    }\n    const nodeStyle = getComputedStyle(node);\n    const node2Style = getComputedStyle(node2);\n    return (\n        !+nodeStyle.padding.replace(NOT_A_NUMBER, '') &&\n        !+node2Style.padding.replace(NOT_A_NUMBER, '') &&\n        !+nodeStyle.margin.replace(NOT_A_NUMBER, '') &&\n        !+node2Style.margin.replace(NOT_A_NUMBER, '')\n    );\n}\n\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\nclass Sanitize {\n    constructor(root) {\n        this.root = root;\n        const rootClosestBlock = closestBlock(root);\n        if (rootClosestBlock) {\n            // Remove unique ids from checklists. These will be renewed afterwards.\n            for (const node of rootClosestBlock.querySelectorAll('[id^=checklist-id-]')) {\n                node.removeAttribute('id');\n            }\n        }\n        this.parse(root);\n        if (rootClosestBlock) {\n            // Ensure unique ids on checklists and stars.\n            for (const node of rootClosestBlock.querySelectorAll('.o_checklist > li')) {\n                node.setAttribute('id', `checklist-id-${Math.floor(new Date() * Math.random())}`);\n            }\n        }\n    }\n\n    parse(node) {\n        node = closestBlock(node);\n        if (node && ['UL', 'OL'].includes(node.tagName)) {\n            node = node.parentElement;\n        }\n        this._parse(node);\n    }\n\n    _parse(node) {\n        while (node) {\n            // Merge identical elements together\n            while (\n                areSimilarElements(node, node.previousSibling) &&\n                !isUnbreakable(node)\n            ) {\n                getDeepRange(this.root, { select: true });\n                const restoreCursor = shouldPreserveCursor(node, this.root) && preserveCursor(this.root.ownerDocument);\n                const nodeP = node.previousSibling;\n                moveNodes(...endPos(node.previousSibling), node);\n                if (restoreCursor) {\n                    restoreCursor();\n                }\n                node = nodeP;\n            }\n\n            const sel = this.root.ownerDocument.getSelection();\n            const anchor = sel && this.root.ownerDocument.getSelection().anchorNode;\n            const anchorEl = anchor && closestElement(anchor);\n            // Remove zero-width spaces added by `fillEmpty` when there is\n            // content and the selection is not next to it.\n            if (\n                node.nodeType === Node.TEXT_NODE &&\n                node.textContent.includes('\\u200B') &&\n                node.parentElement.hasAttribute('oe-zws-empty-inline') &&\n                (\n                    node.textContent.length > 1 ||\n                    // There can be multiple ajacent text nodes, in which case\n                    // the zero-width space is not needed either, despite being\n                    // alone (length === 1) in its own text node.\n                    Array.from(node.parentNode.childNodes).find(\n                        sibling =>\n                            sibling !== node &&\n                            sibling.nodeType === Node.TEXT_NODE &&\n                            sibling.length > 0\n                    )\n                ) &&\n                !isBlock(node.parentElement) &&\n                anchor !== node\n            ) {\n                const restoreCursor = shouldPreserveCursor(node, this.root) && preserveCursor(this.root.ownerDocument);\n                node.textContent = node.textContent.replace('\\u200B', '');\n                node.parentElement.removeAttribute(\"oe-zws-empty-inline\");\n                if (restoreCursor) {\n                    restoreCursor();\n                }\n            }\n\n            // Remove empty blocks in <li>\n            if (\n                node.nodeName === 'P' &&\n                node.parentElement.tagName === 'LI' &&\n                isEmptyBlock(node)\n            ) {\n                const parent = node.parentElement;\n                const restoreCursor = shouldPreserveCursor(node, this.root) && preserveCursor(this.root.ownerDocument);\n                node.remove();\n                fillEmpty(parent);\n                if (restoreCursor) {\n                    restoreCursor(new Map([[node, parent]]));\n                }\n            }\n\n            // Transform <li> into <p> if they are not in a <ul> / <ol>\n            if (node.nodeName === 'LI' && !node.closest('ul, ol')) {\n                const paragraph = document.createElement(\"p\");\n                paragraph.replaceChildren(...node.childNodes);\n                node.replaceWith(paragraph);\n                node = paragraph;\n            }\n\n            // Ensure a zero width space is present inside the FA element.\n            if (isFontAwesome(node) && node.textContent !== '\\u200B') {\n                node.textContent = '\\u200B';\n            }\n\n            // Ensure elements which should not contain any content are tagged\n            // contenteditable=false to avoid any hiccup.\n            if (\n                isVoidElement(node) &&\n                node.getAttribute('contenteditable') !== 'false'\n            ) {\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            let firstChild = node.firstChild;\n            // Unwrap the contents of SPAN and FONT elements without attributes.\n            if (['SPAN', 'FONT'].includes(node.nodeName) && !node.hasAttributes()) {\n                getDeepRange(this.root, { select: true });\n                const restoreCursor = shouldPreserveCursor(node, this.root) && preserveCursor(this.root.ownerDocument);\n                firstChild = unwrapContents(node)[0];\n                if (restoreCursor) {\n                    restoreCursor();\n                }\n            }\n\n            if (firstChild) {\n                this._parse(firstChild);\n            }\n\n            // Update link URL if label is a new valid link.\n            if (node.nodeName === 'A' && anchorEl === node) {\n                const linkLabel = node.innerText;\n                const urlInfo = getUrlsInfosInString(linkLabel);\n                if (urlInfo.length && urlInfo[0].label === linkLabel && !node.href.startsWith('mailto:')) {\n                    node.setAttribute('href', urlInfo[0].url);\n                }\n            }\n            node = node.nextSibling;\n        }\n\n    }\n}\n\nexport function sanitize(root) {\n    new Sanitize(root);\n    return root;\n}\n", "/** @odoo-module **/\n// TODO: avoid empty keys when not necessary to reduce request size\nexport function serializeNode(node, nodesToStripFromChildren = new Set()) {\n    let result = {\n        nodeType: node.nodeType,\n        oid: node.oid,\n    };\n    if (!node.oid) {\n        throw new Error('node.oid can not be falsy.');\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        while (child) {\n            if (!nodesToStripFromChildren.has(child.oid)) {\n                result.children.push(serializeNode(child, nodesToStripFromChildren));\n            }\n            child = child.nextSibling;\n        }\n    }\n    return result;\n}\n\nexport function unserializeNode(obj) {\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    result.oid = obj.oid;\n    return result;\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", "/*! @license DOMPurify 2.3.1 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/2.3.1/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 = global || self, global.DOMPurify = factory());\n}(this, function () { 'use strict';\n\n  function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\n  var hasOwnProperty = Object.hasOwnProperty,\n      setPrototypeOf = Object.setPrototypeOf,\n      isFrozen = Object.isFrozen,\n      getPrototypeOf = Object.getPrototypeOf,\n      getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;\n  var freeze = Object.freeze,\n      seal = Object.seal,\n      create = Object.create; // eslint-disable-line import/no-mutable-exports\n\n  var _ref = typeof Reflect !== 'undefined' && Reflect,\n      apply = _ref.apply,\n      construct = _ref.construct;\n\n  if (!apply) {\n    apply = function apply(fun, thisValue, args) {\n      return fun.apply(thisValue, args);\n    };\n  }\n\n  if (!freeze) {\n    freeze = function freeze(x) {\n      return x;\n    };\n  }\n\n  if (!seal) {\n    seal = function seal(x) {\n      return x;\n    };\n  }\n\n  if (!construct) {\n    construct = function construct(Func, args) {\n      return new (Function.prototype.bind.apply(Func, [null].concat(_toConsumableArray(args))))();\n    };\n  }\n\n  var arrayForEach = unapply(Array.prototype.forEach);\n  var arrayPop = unapply(Array.prototype.pop);\n  var arrayPush = unapply(Array.prototype.push);\n\n  var stringToLowerCase = unapply(String.prototype.toLowerCase);\n  var stringMatch = unapply(String.prototype.match);\n  var stringReplace = unapply(String.prototype.replace);\n  var stringIndexOf = unapply(String.prototype.indexOf);\n  var stringTrim = unapply(String.prototype.trim);\n\n  var regExpTest = unapply(RegExp.prototype.test);\n\n  var typeErrorCreate = unconstruct(TypeError);\n\n  function unapply(func) {\n    return function (thisArg) {\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      return apply(func, thisArg, args);\n    };\n  }\n\n  function unconstruct(func) {\n    return function () {\n      for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {\n        args[_key2] = arguments[_key2];\n      }\n\n      return construct(func, args);\n    };\n  }\n\n  /* Add properties to a lookup table */\n  function addToSet(set, array) {\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\n    var l = array.length;\n    while (l--) {\n      var element = array[l];\n      if (typeof element === 'string') {\n        var lcElement = stringToLowerCase(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\n          element = lcElement;\n        }\n      }\n\n      set[element] = true;\n    }\n\n    return set;\n  }\n\n  /* Shallow clone an object */\n  function clone(object) {\n    var newObject = create(null);\n\n    var property = void 0;\n    for (property in object) {\n      if (apply(hasOwnProperty, object, [property])) {\n        newObject[property] = object[property];\n      }\n    }\n\n    return newObject;\n  }\n\n  /* IE10 doesn't support __lookupGetter__ so lets'\n   * simulate it. It also automatically checks\n   * if the prop is function or getter and behaves\n   * accordingly. */\n  function lookupGetter(object, prop) {\n    while (object !== null) {\n      var desc = getOwnPropertyDescriptor(object, prop);\n      if (desc) {\n        if (desc.get) {\n          return unapply(desc.get);\n        }\n\n        if (typeof desc.value === 'function') {\n          return unapply(desc.value);\n        }\n      }\n\n      object = getPrototypeOf(object);\n    }\n\n    function fallbackValue(element) {\n      console.warn('fallback value for', element);\n      return null;\n    }\n\n    return fallbackValue;\n  }\n\n  var html = 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  var svg = 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\n  var svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', '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  var svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'fedropshadow', 'feimage', '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\n  var mathMl = 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']);\n\n  // Similarly to SVG, we want to know all MathML elements,\n  // even those that we disallow by default.\n  var mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'none']);\n\n  var text = freeze(['#text']);\n\n  var html$1 = 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', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', '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', 'xmlns', 'slot']);\n\n  var svg$1 = 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', '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\n  var mathMl$1 = 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\n  var xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']);\n\n  // eslint-disable-next-line unicorn/better-regex\n  var MUSTACHE_EXPR = seal(/\\{\\{[\\s\\S]*|[\\s\\S]*\\}\\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode\n  var ERB_EXPR = seal(/<%[\\s\\S]*|[\\s\\S]*%>/gm);\n  var DATA_ATTR = seal(/^data-[\\-\\w.\\u00B7-\\uFFFF]/); // eslint-disable-line no-useless-escape\n  var ARIA_ATTR = seal(/^aria-[\\-\\w]+$/); // eslint-disable-line no-useless-escape\n  var IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\\-]+(?:[^a-z+.\\-:]|$))/i // eslint-disable-line no-useless-escape\n  );\n  var IS_SCRIPT_OR_DATA = seal(/^(?:\\w+script|data):/i);\n  var ATTR_WHITESPACE = seal(/[\\u0000-\\u0020\\u00A0\\u1680\\u180E\\u2000-\\u2029\\u205F\\u3000]/g // eslint-disable-line no-control-regex\n  );\n\n  var _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\n  function _toConsumableArray$1(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\n  var 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 {Document} document The document object (to determine policy name suffix)\n   * @return {?TrustedTypePolicy} The policy created (or null, if Trusted Types\n   * are not supported).\n   */\n  var _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, document) {\n    if ((typeof trustedTypes === 'undefined' ? 'undefined' : _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    var suffix = null;\n    var ATTR_NAME = 'data-tt-policy-suffix';\n    if (document.currentScript && document.currentScript.hasAttribute(ATTR_NAME)) {\n      suffix = document.currentScript.getAttribute(ATTR_NAME);\n    }\n\n    var policyName = 'dompurify' + (suffix ? '#' + suffix : '');\n\n    try {\n      return trustedTypes.createPolicy(policyName, {\n        createHTML: function createHTML(html$$1) {\n          return html$$1;\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\n  function createDOMPurify() {\n    var window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal();\n\n    var DOMPurify = function DOMPurify(root) {\n      return createDOMPurify(root);\n    };\n\n    /**\n     * Version label, exposed for easier checks\n     * if DOMPurify is up to date or not\n     */\n    DOMPurify.version = '2.3.1';\n\n    /**\n     * Array of elements that DOMPurify removed during sanitation.\n     * Empty if nothing was removed.\n     */\n    DOMPurify.removed = [];\n\n    if (!window || !window.document || window.document.nodeType !== 9) {\n      // Not running in a browser, provide a factory function\n      // so that you can pass your own Window\n      DOMPurify.isSupported = false;\n\n      return DOMPurify;\n    }\n\n    var originalDocument = window.document;\n\n    var document = window.document;\n    var DocumentFragment = window.DocumentFragment,\n        HTMLTemplateElement = window.HTMLTemplateElement,\n        Node = window.Node,\n        Element = window.Element,\n        NodeFilter = window.NodeFilter,\n        _window$NamedNodeMap = window.NamedNodeMap,\n        NamedNodeMap = _window$NamedNodeMap === undefined ? window.NamedNodeMap || window.MozNamedAttrMap : _window$NamedNodeMap,\n        Text = window.Text,\n        Comment = window.Comment,\n        DOMParser = window.DOMParser,\n        trustedTypes = window.trustedTypes;\n\n\n    var ElementPrototype = Element.prototype;\n\n    var cloneNode = lookupGetter(ElementPrototype, 'cloneNode');\n    var getNextSibling = lookupGetter(ElementPrototype, 'nextSibling');\n    var getChildNodes = lookupGetter(ElementPrototype, 'childNodes');\n    var 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      var template = document.createElement('template');\n      if (template.content && template.content.ownerDocument) {\n        document = template.content.ownerDocument;\n      }\n    }\n\n    var trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, originalDocument);\n    var emptyHTML = trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML('') : '';\n\n    var _document = document,\n        implementation = _document.implementation,\n        createNodeIterator = _document.createNodeIterator,\n        createDocumentFragment = _document.createDocumentFragment,\n        getElementsByTagName = _document.getElementsByTagName;\n    var importNode = originalDocument.importNode;\n\n\n    var documentMode = {};\n    try {\n      documentMode = clone(document).documentMode ? document.documentMode : {};\n    } catch (_) {}\n\n    var hooks = {};\n\n    /**\n     * Expose whether this browser supports running the full DOMPurify.\n     */\n    DOMPurify.isSupported = typeof getParentNode === 'function' && implementation && typeof implementation.createHTMLDocument !== 'undefined' && documentMode !== 9;\n\n    var MUSTACHE_EXPR$$1 = MUSTACHE_EXPR,\n        ERB_EXPR$$1 = ERB_EXPR,\n        DATA_ATTR$$1 = DATA_ATTR,\n        ARIA_ATTR$$1 = ARIA_ATTR,\n        IS_SCRIPT_OR_DATA$$1 = IS_SCRIPT_OR_DATA,\n        ATTR_WHITESPACE$$1 = ATTR_WHITESPACE;\n    var IS_ALLOWED_URI$$1 = IS_ALLOWED_URI;\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\n    var ALLOWED_TAGS = null;\n    var DEFAULT_ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(html), _toConsumableArray$1(svg), _toConsumableArray$1(svgFilters), _toConsumableArray$1(mathMl), _toConsumableArray$1(text)));\n\n    /* Allowed attribute names */\n    var ALLOWED_ATTR = null;\n    var DEFAULT_ALLOWED_ATTR = addToSet({}, [].concat(_toConsumableArray$1(html$1), _toConsumableArray$1(svg$1), _toConsumableArray$1(mathMl$1), _toConsumableArray$1(xml)));\n\n    /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */\n    var FORBID_TAGS = null;\n\n    /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */\n    var FORBID_ATTR = null;\n\n    /* Decide if ARIA attributes are okay */\n    var ALLOW_ARIA_ATTR = true;\n\n    /* Decide if custom data attributes are okay */\n    var ALLOW_DATA_ATTR = true;\n\n    /* Decide if unknown protocols are okay */\n    var ALLOW_UNKNOWN_PROTOCOLS = false;\n\n    /* Output should be safe for common template engines.\n     * This means, DOMPurify removes data attributes, mustaches and ERB\n     */\n    var SAFE_FOR_TEMPLATES = false;\n\n    /* Decide if document with <html>... should be returned */\n    var WHOLE_DOCUMENT = false;\n\n    /* Track whether config is already set on this instance of DOMPurify. */\n    var 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    var 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    var 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    var RETURN_DOM_FRAGMENT = false;\n\n    /* If `RETURN_DOM` or `RETURN_DOM_FRAGMENT` is enabled, decide if the returned DOM\n     * `Node` is imported into the current `Document`. If this flag is not enabled the\n     * `Node` will belong (its ownerDocument) to a fresh `HTMLDocument`, created by\n     * DOMPurify.\n     *\n     * This defaults to `true` starting DOMPurify 2.2.0. Note that setting it to `false`\n     * might cause XSS from attacks hidden in closed shadowroots in case the browser\n     * supports Declarative Shadow: DOM https://web.dev/declarative-shadow-dom/\n     */\n    var RETURN_DOM_IMPORT = true;\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    var RETURN_TRUSTED_TYPE = false;\n\n    /* Output should be free from DOM clobbering attacks? */\n    var SANITIZE_DOM = true;\n\n    /* Keep element content when removing element? */\n    var 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    var IN_PLACE = false;\n\n    /* Allow usage of profiles like html, svg and mathMl */\n    var USE_PROFILES = {};\n\n    /* Tags to ignore content of when KEEP_CONTENT is true */\n    var FORBID_CONTENTS = null;\n    var 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    var DATA_URI_TAGS = null;\n    var DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']);\n\n    /* Attributes safe for values like \"javascript:\" */\n    var URI_SAFE_ATTRIBUTES = null;\n    var DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']);\n\n    var MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML';\n    var SVG_NAMESPACE = 'http://www.w3.org/2000/svg';\n    var HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml';\n    /* Document namespace */\n    var NAMESPACE = HTML_NAMESPACE;\n    var IS_EMPTY_INPUT = false;\n\n    /* Keep a reference to config to pass to hooks */\n    var CONFIG = null;\n\n    /* Ideally, do not touch anything below this line */\n    /* ______________________________________________ */\n\n    var formElement = document.createElement('form');\n\n    /**\n     * _parseConfig\n     *\n     * @param  {Object} cfg optional config literal\n     */\n    // eslint-disable-next-line complexity\n    var _parseConfig = function _parseConfig(cfg) {\n      if (CONFIG && CONFIG === cfg) {\n        return;\n      }\n\n      /* Shield configuration object from tampering */\n      if (!cfg || (typeof cfg === 'undefined' ? 'undefined' : _typeof(cfg)) !== 'object') {\n        cfg = {};\n      }\n\n      /* Shield configuration object from prototype pollution */\n      cfg = clone(cfg);\n\n      /* Set configuration parameters */\n      ALLOWED_TAGS = 'ALLOWED_TAGS' in cfg ? addToSet({}, cfg.ALLOWED_TAGS) : DEFAULT_ALLOWED_TAGS;\n      ALLOWED_ATTR = 'ALLOWED_ATTR' in cfg ? addToSet({}, cfg.ALLOWED_ATTR) : DEFAULT_ALLOWED_ATTR;\n      URI_SAFE_ATTRIBUTES = 'ADD_URI_SAFE_ATTR' in cfg ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR) : DEFAULT_URI_SAFE_ATTRIBUTES;\n      DATA_URI_TAGS = 'ADD_DATA_URI_TAGS' in cfg ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS) : DEFAULT_DATA_URI_TAGS;\n      FORBID_CONTENTS = 'FORBID_CONTENTS' in cfg ? addToSet({}, cfg.FORBID_CONTENTS) : DEFAULT_FORBID_CONTENTS;\n      FORBID_TAGS = 'FORBID_TAGS' in cfg ? addToSet({}, cfg.FORBID_TAGS) : {};\n      FORBID_ATTR = 'FORBID_ATTR' in cfg ? addToSet({}, cfg.FORBID_ATTR) : {};\n      USE_PROFILES = 'USE_PROFILES' in cfg ? 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      SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false\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_DOM_IMPORT = cfg.RETURN_DOM_IMPORT !== false; // Default true\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      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$$1;\n      NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE;\n      if (SAFE_FOR_TEMPLATES) {\n        ALLOW_DATA_ATTR = false;\n      }\n\n      if (RETURN_DOM_FRAGMENT) {\n        RETURN_DOM = true;\n      }\n\n      /* Parse profile info */\n      if (USE_PROFILES) {\n        ALLOWED_TAGS = addToSet({}, [].concat(_toConsumableArray$1(text)));\n        ALLOWED_ATTR = [];\n        if (USE_PROFILES.html === true) {\n          addToSet(ALLOWED_TAGS, html);\n          addToSet(ALLOWED_ATTR, html$1);\n        }\n\n        if (USE_PROFILES.svg === true) {\n          addToSet(ALLOWED_TAGS, svg);\n          addToSet(ALLOWED_ATTR, svg$1);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n\n        if (USE_PROFILES.svgFilters === true) {\n          addToSet(ALLOWED_TAGS, svgFilters);\n          addToSet(ALLOWED_ATTR, svg$1);\n          addToSet(ALLOWED_ATTR, xml);\n        }\n\n        if (USE_PROFILES.mathMl === true) {\n          addToSet(ALLOWED_TAGS, mathMl);\n          addToSet(ALLOWED_ATTR, mathMl$1);\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\n        addToSet(ALLOWED_TAGS, cfg.ADD_TAGS);\n      }\n\n      if (cfg.ADD_ATTR) {\n        if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) {\n          ALLOWED_ATTR = clone(ALLOWED_ATTR);\n        }\n\n        addToSet(ALLOWED_ATTR, cfg.ADD_ATTR);\n      }\n\n      if (cfg.ADD_URI_SAFE_ATTR) {\n        addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR);\n      }\n\n      if (cfg.FORBID_CONTENTS) {\n        if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) {\n          FORBID_CONTENTS = clone(FORBID_CONTENTS);\n        }\n\n        addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS);\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\n      // Prevent further manipulation of configuration.\n      // Not available in IE8, Safari 5, etc.\n      if (freeze) {\n        freeze(cfg);\n      }\n\n      CONFIG = cfg;\n    };\n\n    var MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']);\n\n    var HTML_INTEGRATION_POINTS = addToSet({}, ['foreignobject', 'desc', 'title', 'annotation-xml']);\n\n    /* Keep track of all possible SVG and MathML tags\n     * so that we can perform the namespace checks\n     * correctly. */\n    var ALL_SVG_TAGS = addToSet({}, svg);\n    addToSet(ALL_SVG_TAGS, svgFilters);\n    addToSet(ALL_SVG_TAGS, svgDisallowed);\n\n    var ALL_MATHML_TAGS = addToSet({}, mathMl);\n    addToSet(ALL_MATHML_TAGS, mathMlDisallowed);\n\n    /**\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    var _checkValidNamespace = function _checkValidNamespace(element) {\n      var 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: HTML_NAMESPACE,\n          tagName: 'template'\n        };\n      }\n\n      var tagName = stringToLowerCase(element.tagName);\n      var parentTagName = stringToLowerCase(parent.tagName);\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\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\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\n        if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) {\n          return false;\n        }\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 erronously deleted from\n        // HTML namespace.\n        var commonSvgAndHTMLElements = addToSet({}, ['title', 'style', 'font', 'a', 'script']);\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] && (commonSvgAndHTMLElements[tagName] || !ALL_SVG_TAGS[tagName]);\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 or MathML). Return false just in case.\n      return false;\n    };\n\n    /**\n     * _forceRemove\n     *\n     * @param  {Node} node a DOM node\n     */\n    var _forceRemove = function _forceRemove(node) {\n      arrayPush(DOMPurify.removed, { element: node });\n      try {\n        // eslint-disable-next-line unicorn/prefer-dom-node-remove\n        node.parentNode.removeChild(node);\n      } catch (_) {\n        try {\n          node.outerHTML = emptyHTML;\n        } catch (_) {\n          node.remove();\n        }\n      }\n    };\n\n    /**\n     * _removeAttribute\n     *\n     * @param  {String} name an Attribute name\n     * @param  {Node} node a DOM node\n     */\n    var _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\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    var _initDocument = function _initDocument(dirty) {\n      /* Create a HTML document */\n      var doc = void 0;\n      var leadingWhitespace = void 0;\n\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        var matches = stringMatch(dirty, /^[\\r\\n\\t ]+/);\n        leadingWhitespace = matches && matches[0];\n      }\n\n      var 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, 'text/html');\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 ? '' : dirtyPayload;\n        } catch (_) {\n          // Syntax error if dirtyPayload is invalid xml\n        }\n      }\n\n      var body = doc.body || doc.documentElement;\n\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\n      return WHOLE_DOCUMENT ? doc.documentElement : body;\n    };\n\n    /**\n     * _createIterator\n     *\n     * @param  {Document} root document/fragment to create iterator for\n     * @return {Iterator} iterator instance\n     */\n    var _createIterator = function _createIterator(root) {\n      return createNodeIterator.call(root.ownerDocument || root, root, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT, null, false);\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    var _isClobbered = function _isClobbered(elm) {\n      if (elm instanceof Text || elm instanceof Comment) {\n        return false;\n      }\n\n      if (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') {\n        return true;\n      }\n\n      return false;\n    };\n\n    /**\n     * _isNode\n     *\n     * @param  {Node} obj object to check whether it's a DOM node\n     * @return {Boolean} true is object is a DOM node\n     */\n    var _isNode = function _isNode(object) {\n      return (typeof Node === 'undefined' ? 'undefined' : _typeof(Node)) === 'object' ? object instanceof Node : object && (typeof object === 'undefined' ? 'undefined' : _typeof(object)) === 'object' && typeof object.nodeType === 'number' && typeof object.nodeName === 'string';\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    var _executeHook = function _executeHook(entryPoint, currentNode, data) {\n      if (!hooks[entryPoint]) {\n        return;\n      }\n\n      arrayForEach(hooks[entryPoint], function (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    var _sanitizeElements = function _sanitizeElements(currentNode) {\n      var content = void 0;\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      /* Check if tagname contains Unicode */\n      if (stringMatch(currentNode.nodeName, /[\\u0080-\\uFFFF]/)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Now let's check the element's type and name */\n      var tagName = stringToLowerCase(currentNode.nodeName);\n\n      /* Execute a hook if present */\n      _executeHook('uponSanitizeElement', currentNode, {\n        tagName: tagName,\n        allowedTags: ALLOWED_TAGS\n      });\n\n      /* Detect mXSS attempts abusing namespace confusion */\n      if (!_isNode(currentNode.firstElementChild) && (!_isNode(currentNode.content) || !_isNode(currentNode.content.firstElementChild)) && regExpTest(/<[/\\w]/g, currentNode.innerHTML) && regExpTest(/<[/\\w]/g, currentNode.textContent)) {\n        _forceRemove(currentNode);\n        return true;\n      }\n\n      /* Mitigate a problem with templates inside select */\n      if (tagName === 'select' && regExpTest(/<template/i, currentNode.innerHTML)) {\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        /* Keep content except for bad-listed elements */\n        if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) {\n          var parentNode = getParentNode(currentNode) || currentNode.parentNode;\n          var childNodes = getChildNodes(currentNode) || currentNode.childNodes;\n\n          if (childNodes && parentNode) {\n            var childCount = childNodes.length;\n\n            for (var i = childCount - 1; i >= 0; --i) {\n              parentNode.insertBefore(cloneNode(childNodes[i], true), getNextSibling(currentNode));\n            }\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      if ((tagName === 'noscript' || tagName === 'noembed') && regExpTest(/<\\/no(script|embed)/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 === 3) {\n        /* Get the element's text content */\n        content = currentNode.textContent;\n        content = stringReplace(content, MUSTACHE_EXPR$$1, ' ');\n        content = stringReplace(content, ERB_EXPR$$1, ' ');\n        if (currentNode.textContent !== content) {\n          arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() });\n          currentNode.textContent = content;\n        }\n      }\n\n      /* Execute a hook if present */\n      _executeHook('afterSanitizeElements', currentNode, null);\n\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    var _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$$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$$1, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) {\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$$1, ''))) ; 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$$1, stringReplace(value, ATTR_WHITESPACE$$1, ''))) ; else if (!value) ; else {\n        return false;\n      }\n\n      return true;\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    var _sanitizeAttributes = function _sanitizeAttributes(currentNode) {\n      var attr = void 0;\n      var value = void 0;\n      var lcName = void 0;\n      var l = void 0;\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeAttributes', currentNode, null);\n\n      var attributes = currentNode.attributes;\n\n      /* Check if we have attributes; if not we might have a text node */\n\n      if (!attributes) {\n        return;\n      }\n\n      var hookEvent = {\n        attrName: '',\n        attrValue: '',\n        keepAttr: true,\n        allowedAttributes: ALLOWED_ATTR\n      };\n      l = attributes.length;\n\n      /* Go backwards over all attributes; safely remove bad ones */\n      while (l--) {\n        attr = attributes[l];\n        var _attr = attr,\n            name = _attr.name,\n            namespaceURI = _attr.namespaceURI;\n\n        value = stringTrim(attr.value);\n        lcName = stringToLowerCase(name);\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 (regExpTest(/\\/>/i, value)) {\n          _removeAttribute(name, currentNode);\n          continue;\n        }\n\n        /* Sanitize attribute content to be template-safe */\n        if (SAFE_FOR_TEMPLATES) {\n          value = stringReplace(value, MUSTACHE_EXPR$$1, ' ');\n          value = stringReplace(value, ERB_EXPR$$1, ' ');\n        }\n\n        /* Is `value` valid for this attribute? */\n        var lcTag = currentNode.nodeName.toLowerCase();\n        if (!_isValidAttribute(lcTag, lcName, value)) {\n          continue;\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\n          arrayPop(DOMPurify.removed);\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    var _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) {\n      var shadowNode = void 0;\n      var shadowIterator = _createIterator(fragment);\n\n      /* Execute a hook if present */\n      _executeHook('beforeSanitizeShadowDOM', fragment, null);\n\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} configuration object\n     */\n    // eslint-disable-next-line complexity\n    DOMPurify.sanitize = function (dirty, cfg) {\n      var body = void 0;\n      var importedNode = void 0;\n      var currentNode = void 0;\n      var oldNode = void 0;\n      var returnNode = void 0;\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        // eslint-disable-next-line no-negated-condition\n        if (typeof dirty.toString !== 'function') {\n          throw typeErrorCreate('toString is not a function');\n        } else {\n          dirty = dirty.toString();\n          if (typeof dirty !== 'string') {\n            throw typeErrorCreate('dirty is not a string, aborting');\n          }\n        }\n      }\n\n      /* Check we can run. Otherwise fall back or ignore */\n      if (!DOMPurify.isSupported) {\n        if (_typeof(window.toStaticHTML) === 'object' || typeof window.toStaticHTML === 'function') {\n          if (typeof dirty === 'string') {\n            return window.toStaticHTML(dirty);\n          }\n\n          if (_isNode(dirty)) {\n            return window.toStaticHTML(dirty.outerHTML);\n          }\n        }\n\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\n      if (IN_PLACE) ; 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 === 1 && 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 : 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      var nodeIterator = _createIterator(IN_PLACE ? dirty : body);\n\n      /* Now start iterating over the created document */\n      while (currentNode = nodeIterator.nextNode()) {\n        /* Fix IE's strange behavior with manipulated textNodes #89 */\n        if (currentNode.nodeType === 3 && currentNode === oldNode) {\n          continue;\n        }\n\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        oldNode = currentNode;\n      }\n\n      oldNode = null;\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\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\n        if (RETURN_DOM_IMPORT) {\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\n        return returnNode;\n      }\n\n      var serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML;\n\n      /* Sanitize final string template-safe */\n      if (SAFE_FOR_TEMPLATES) {\n        serializedHTML = stringReplace(serializedHTML, MUSTACHE_EXPR$$1, ' ');\n        serializedHTML = stringReplace(serializedHTML, ERB_EXPR$$1, ' ');\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 (cfg) {\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\n      var lcTag = stringToLowerCase(tag);\n      var lcName = stringToLowerCase(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\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     */\n    DOMPurify.removeHook = function (entryPoint) {\n      if (hooks[entryPoint]) {\n        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     */\n    DOMPurify.removeAllHooks = function () {\n      hooks = {};\n    };\n\n    return DOMPurify;\n  }\n\n  var purify = createDOMPurify();\n\n  return purify;\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 keydown = e => {\n            const actions = {\n                ArrowRight: {\n                    colNumber: this.colNumber + 1,\n                    rowNumber: this.rowNumber,\n                },\n                ArrowLeft: {\n                    colNumber: this.colNumber - 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 parentContextRect = this.options.getContextFromParentRect();\n        const offset = getRangePosition(this.el, this.options.document);\n        this.el.style.left = `${parentContextRect.left + offset.left}px`;\n        this.el.style.top = `${parentContextRect.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\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\nexport class Powerbox {\n    constructor(options = {}) {\n        this._active = false;\n        this.options = options;\n        this.options.width = this.options.width || 340;\n        if (!this.options._t) this.options._t = string => string;\n\n        this.el = document.createElement('div');\n        this.el.className = 'oe-commandbar-wrapper';\n        this.el.style.display = 'none';\n        this.el.style.width = `${this.options.width}px`;\n        document.body.append(this.el);\n\n        this.addHotKey('/', { commands: this.options.commands });\n\n        this._mainWrapperElement = document.createElement('div');\n        this._mainWrapperElement.className = 'oe-commandbar-mainWrapper';\n        this.el.append(this._mainWrapperElement);\n        this.el.addEventListener('mousedown', event => {\n            event.stopPropagation();\n        });\n    }\n\n    destroy() {\n        this.el.remove();\n    }\n\n    render(commands) {\n        this._mainWrapperElement.innerHTML = '';\n        clearTimeout(this._renderingTimeout);\n        this._hoverActive = false;\n\n        if (commands.length === 0) {\n            const groupWrapperEl = document.createElement('div');\n            groupWrapperEl.className = 'oe-commandbar-groupWrapper';\n            const groupNameEl = document.createElement('div');\n            groupNameEl.className = 'oe-commandbar-noResult';\n            groupWrapperEl.append(groupNameEl);\n            this._mainWrapperElement.append(groupWrapperEl);\n            groupNameEl.innerText = this.options._t('No results');\n            this._resetPosition();\n            return;\n        }\n\n        this._currentSelectedCommand =\n            commands.find(c => c === this._currentSelectedCommand) || commands[0];\n        const groups = {};\n        for (const command of commands) {\n            groups[command.groupName] = groups[command.groupName] || [];\n            groups[command.groupName].push(command);\n        }\n        for (const [groupName, commands] of Object.entries(groups)) {\n            const groupWrapperEl = document.createElement('div');\n            groupWrapperEl.className = 'oe-commandbar-groupWrapper';\n            const groupNameEl = document.createElement('div');\n            groupNameEl.className = 'oe-commandbar-groupName';\n            groupWrapperEl.append(groupNameEl);\n            this._mainWrapperElement.append(groupWrapperEl);\n            groupNameEl.innerText = groupName;\n            for (const command of commands) {\n                const commandElWrapper = document.createElement('div');\n                commandElWrapper.className = 'oe-commandbar-commandWrapper';\n                if (this._currentSelectedCommand === command) {\n                    commandElWrapper.classList.add('active');\n                    // use setTimeout in order to avoid to call it upon the\n                    // first rendering.\n                    this._renderingTimeout = setTimeout(() => {\n                        commandElWrapper.scrollIntoView({\n                            block: 'nearest',\n                            inline: 'nearest',\n                        });\n                    });\n                }\n                let commandImgEl, commandTitleEl, commandDescriptionEl;\n                switch (command.style) {\n                    case 'small':\n                        commandTitleEl = document.createElement('div');\n                        commandTitleEl.setAttribute('class', 'oe-commandbar-commandSmall');\n                        commandTitleEl.setAttribute('title', command.description);\n                        commandTitleEl.innerText = command.title;\n                        commandElWrapper.append(commandTitleEl);\n                        break;\n                    default:\n                        commandElWrapper.innerHTML = `\n                    <div class=\"oe-commandbar-commandLeftCol\">\n                        <i class=\"oe-commandbar-commandImg fa\"></i>\n                    </div>\n                    <div class=\"oe-commandbar-commandRightCol\">\n                        <div class=\"oe-commandbar-commandTitle\">\n                        </div>\n                        <div class=\"oe-commandbar-commandDescription\">\n                        </div>\n                    </div>`;\n                        commandImgEl = commandElWrapper.querySelector('.oe-commandbar-commandImg');\n                        commandTitleEl = commandElWrapper.querySelector(\n                            '.oe-commandbar-commandTitle',\n                        );\n                        commandDescriptionEl = commandElWrapper.querySelector(\n                            '.oe-commandbar-commandDescription',\n                        );\n                        commandTitleEl.innerText = command.title;\n                        commandDescriptionEl.innerText = command.description;\n                        commandImgEl.classList.add(command.fontawesome);\n                }\n\n                groupWrapperEl.append(commandElWrapper);\n\n                const commandElWrapperMouseMove = () => {\n                    if (!this._hoverActive || commandElWrapper.classList.contains('active')) {\n                        return;\n                    }\n                    this.el\n                        .querySelector('.oe-commandbar-commandWrapper.active')\n                        .classList.remove('active');\n                    this._currentSelectedCommand = command;\n                    commandElWrapper.classList.add('active');\n                };\n                commandElWrapper.addEventListener('mousemove', commandElWrapperMouseMove);\n                commandElWrapper.addEventListener(\n                    'mousedown',\n                    ev => {\n                        ev.preventDefault();\n                        ev.stopImmediatePropagation();\n                        this._currentValidate(command);\n                    },\n                    true,\n                );\n            }\n        }\n        // Hide group name if there is only a single group.\n        if (Object.entries(groups).length === 1) {\n            this._mainWrapperElement.querySelector('.oe-commandbar-groupName').style.display = 'none';\n        }\n        this._resetPosition();\n    }\n\n    addHotKey(triggerKey, options) {\n        this.options.editable.addEventListener(\n            'input',\n            ev => {\n                const selection = this.options.document.getSelection();\n                if (!selection.isCollapsed || !selection.rangeCount) return;\n                if (\n                    ev.data === triggerKey &&\n                    !this._active &&\n                    (!this.options.shouldActivate || this.options.shouldActivate())\n                ) {\n                    this.open({ ...options, openOnKeyupTarget: ev.target });\n                }\n            },\n            true,\n        );\n    }\n\n    // todo: remove in master. It has been kept when changing addEventListener\n    // keydown to input in stable to avoid removing a public method.\n    addKeydownTrigger(triggerKey, options) {\n        this.addHotKey(triggerKey, options);\n    }\n\n    open(openOptions) {\n        this.options.onActivate && this.options.onActivate();\n        this._currentOpenOptions = openOptions;\n\n        const openOnKeyupTarget =\n            this._currentOpenOptions.openOnKeyupTarget || this.options.editable;\n        const onValueChangeFunction = term =>\n            this._currentOpenOptions.valueChangeFunction\n                ? this._currentOpenOptions.valueChangeFunction(term)\n                : this._filter(term, this._currentOpenOptions.commands);\n\n        const showOnceOnKeyup = () => {\n            this.show();\n            openOnKeyupTarget.removeEventListener('keyup', showOnceOnKeyup, true);\n            const selection = this.options.document.getSelection();\n            const currentBlock = (selection && closestBlock(selection.anchorNode)) || this.options.editable;\n            initialTarget = currentBlock;\n            this._initialValue = currentBlock.textContent;\n        };\n        openOnKeyupTarget.addEventListener('keyup', showOnceOnKeyup, true);\n\n        this._active = true;\n        this._currentFilteredCommands = this._currentOpenOptions.commands;\n        this.render(this._currentOpenOptions.commands);\n\n        let initialTarget;\n\n        const keyup = async ev => {\n            if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {\n                ev.preventDefault();\n                return;\n            }\n            if (!initialTarget) return;\n            const diff = patienceDiff(\n                this._initialValue.split(''),\n                initialTarget.textContent.split(''),\n                true,\n            );\n            this._lastText = diff.bMove.join('');\n            const selection = this.options.document.getSelection();\n            if (\n                (this._lastText.match(/\\s/) && this._currentOpenOptions.closeOnSpace !== false) ||\n                !selection ||\n                initialTarget !== closestBlock(selection.anchorNode)\n            ) {\n                this._stop();\n                return;\n            }\n            const term = this._lastText;\n\n            this._currentFilteredCommands = await onValueChangeFunction(term);\n            this.render(this._currentFilteredCommands);\n        };\n        const keydown = ev => {\n            if (ev.key === 'Enter') {\n                ev.stopImmediatePropagation();\n                this._currentValidate();\n                ev.preventDefault();\n            } else if (ev.key === 'Escape') {\n                ev.stopImmediatePropagation();\n                this._stop();\n                ev.preventDefault();\n            } else if (ev.key === 'Backspace' && !this._lastText) {\n                this._stop();\n            } else if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {\n                ev.preventDefault();\n                ev.stopImmediatePropagation();\n\n                const index = this._currentFilteredCommands.findIndex(\n                    c => c === this._currentSelectedCommand,\n                );\n                if (!this._currentFilteredCommands.length || index === -1) {\n                    this._currentSelectedCommand = undefined;\n                } else {\n                    const n = ev.key === 'ArrowDown' ? 1 : -1;\n                    const newIndex = cycle(index + n, this._currentFilteredCommands.length - 1);\n                    this._currentSelectedCommand = this._currentFilteredCommands[newIndex];\n                }\n                ev.preventDefault();\n                this.render(this._currentFilteredCommands);\n            }\n        };\n        const mousemove = () => {\n            this._hoverActive = true;\n        };\n\n        this._stop = () => {\n            this._active = false;\n            this.hide();\n            this._currentSelectedCommand = undefined;\n\n            this.options.document.removeEventListener('keyup', keyup);\n            this.options.document.removeEventListener('keydown', keydown, true);\n            this.options.document.removeEventListener('mousemove', mousemove);\n            this.options.document.removeEventListener('mousedown', this._stop);\n            if (document !== this.options.document) {\n                document.removeEventListener('mousemove', mousemove);\n                document.removeEventListener('mousedown', this._stop);\n            }\n\n            this.options.onStop && this.options.onStop();\n        };\n        this._currentValidate = (command) => {\n            if (!command) {\n                command = this._currentFilteredCommands.find(\n                    c => c === this._currentSelectedCommand,\n                );\n            }\n            if (command) {\n                !command.isIntermediateStep &&\n                    (!command.shouldPreValidate || command.shouldPreValidate()) &&\n                    this.options.preValidate &&\n                    this.options.preValidate();\n                command.callback();\n                !command.isIntermediateStep &&\n                    this.options.postValidate &&\n                    this.options.postValidate();\n            }\n            if (!command || !command.isIntermediateStep) {\n                this._stop();\n            }\n        };\n        this.options.document.addEventListener('keyup', keyup);\n        this.options.document.addEventListener('keydown', keydown, true);\n        this.options.document.addEventListener('mousemove', mousemove);\n        this.options.document.addEventListener('mousedown', this._stop);\n        // If the Golbal document is diferent than the provided options.document,\n        // which happend when the editor is inside an Iframe.\n        // We need to listen to the mouse event on both document\n        // to be sure the command bar will always close when clicking outside of it.\n        if (document !== this.options.document) {\n            document.addEventListener('mousemove', mousemove);\n            document.addEventListener('mousedown', this._stop);\n        }\n        // Display powerbox immediately when forceShow is set.\n        if (this._currentOpenOptions.forceShow) showOnceOnKeyup();\n    }\n\n    nextOpenOptions(openOptions) {\n        this._currentOpenOptions = openOptions;\n        this._initialValue = (\n            this._currentOpenOptions.openOnKeyupTarget || this.options.editable\n        ).textContent;\n        this._currentFilteredCommands = this._currentOpenOptions.commands;\n        this.render(this._currentOpenOptions.commands);\n    }\n\n    show() {\n        this.options.onShow && this.options.onShow();\n        this.el.style.display = 'flex';\n        this._resetPosition();\n    }\n\n    hide() {\n        this.el.style.display = 'none';\n        if (this._active) this._stop();\n    }\n\n    // -------------------------------------------------------------------------\n    // private\n    // -------------------------------------------------------------------------\n\n    _filter(term, commands) {\n        const initialCommands = commands.filter(c => !c.isDisabled || !c.isDisabled());\n        if (term === '') {\n            return initialCommands;\n        }\n        term = term.replace(/\\s/g, '\\\\s');\n        term = term.replace(/[\\\\^$.*+?()[\\]{}|]/g, '\\\\$&');\n        const exactRegex = new RegExp(term, 'i');\n        const fuzzyRegex = new RegExp(term.match(/\\\\.|./g).join('.*'), 'i');\n        if (term.length) {\n            commands = initialCommands.filter(command => {\n                const commandText = (command.groupName + ' ' + command.title);\n                const commandDescription = command.description.replace(/\\s/g, '');\n                return commandText.match(fuzzyRegex) || commandDescription.match(exactRegex);\n            });\n        }\n        return commands;\n    }\n\n    _resetPosition() {\n        const position = getRangePosition(this.el, this.options.document, this.options);\n        if (!position) {\n            this.hide();\n            return;\n        }\n        let { left, top } = position;\n\n        this.el.style.left = `${left}px`;\n        this.el.style.top = `${top}px`;\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 {\n    ancestors,\n    descendants,\n    childNodeIndex,\n    closestBlock,\n    closestElement,\n    closestPath,\n    DIRECTIONS,\n    findNode,\n    getCursors,\n    getDeepRange,\n    getInSelection,\n    getListMode,\n    getSelectedNodes,\n    getTraversedNodes,\n    insertAndSelectZws,\n    insertText,\n    isBlock,\n    isColorGradient,\n    isContentTextNode,\n    isSelectionFormat,\n    isShrunkBlock,\n    isVisible,\n    isVisibleStr,\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    formatSelection,\n    getDeepestPosition,\n    fillEmpty,\n    isEmptyBlock,\n    unwrapContents,\n    getCursorDirection,\n    firstLeaf,\n    lastLeaf,\n} from '../utils/utils.js';\n\nconst TEXT_CLASSES_REGEX = /\\btext-[^\\s]*\\b/;\nconst BG_CLASSES_REGEX = /\\bbg-[^\\s]*\\b/;\n\nfunction insert(editor, data, isText = true) {\n    if (!data) {\n        return;\n    }\n    const selection = editor.document.getSelection();\n    const range = selection.getRangeAt(0);\n    let startNode;\n    let insertBefore = false;\n    if (selection.isCollapsed) {\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    } else {\n        editor.deleteRange(selection);\n    }\n\n    const fakeEl = document.createElement('fake-element');\n    const fakeElFirstChild = document.createElement('fake-element-fc');\n    const fakeElLastChild = document.createElement('fake-element-lc');\n    if (data instanceof Node) {\n        fakeEl.replaceChildren(data);\n    } else if (isText) {\n        fakeEl.innerText = data;\n    } else {\n        fakeEl.innerHTML = data;\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    if (closestElement(selection.anchorNode, 'UL, OL') &&\n        (fakeEl.firstChild.nodeName === 'UL' || fakeEl.firstChild.nodeName === 'OL')) {\n       fakeEl.replaceChildren(...fakeEl.firstChild.childNodes);\n    }\n\n    startNode = startNode || editor.document.getSelection().anchorNode;\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 (fakeEl.childElementCount === 1 && (\n        fakeEl.firstChild.nodeName === 'P' ||\n        fakeEl.firstChild.nodeName === 'LI' ||\n        fakeEl.firstChild.nodeName === 'PRE' && closestElement(startNode, 'pre')\n    )) {\n        const p = fakeEl.firstElementChild;\n        fakeEl.replaceChildren(...p.childNodes);\n    } else if (fakeEl.childElementCount > 1) {\n        // Grab the content of the first child block and isolate it.\n        if (isBlock(fakeEl.firstChild) && !['TABLE', 'UL', 'OL'].includes(fakeEl.firstChild.nodeName)) {\n            fakeElFirstChild.replaceChildren(...fakeEl.firstElementChild.childNodes);\n            fakeEl.firstElementChild.remove();\n        }\n        // Grab the content of the last child block and isolate it.\n        if (isBlock(fakeEl.lastChild) && !['TABLE', 'UL', 'OL'].includes(fakeEl.lastChild.nodeName)) {\n            fakeElLastChild.replaceChildren(...fakeEl.lastElementChild.childNodes);\n            fakeEl.lastElementChild.remove();\n        }\n    }\n\n    if (startNode.nodeType === Node.ELEMENT_NODE) {\n        if (selection.anchorOffset === 0) {\n            const textNode = editor.document.createTextNode('');\n            startNode.prepend(textNode);\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 _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    if (fakeElLastChild.hasChildNodes()) {\n        const toInsert = [...fakeElLastChild.childNodes]; // Prevent mutation\n        _insertAt(currentNode, [...toInsert], insertBefore);\n        currentNode = insertBefore ? toInsert[0] : currentNode;\n        lastChildNode = toInsert[toInsert.length - 1];\n    }\n    if (fakeElFirstChild.hasChildNodes()) {\n        const toInsert = [...fakeElFirstChild.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 (!fakeEl.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            currentNode.nextSibling.parentElement.oEnter(index);\n        }\n    }\n\n    let nodeToInsert;\n    const insertedNodes = [...fakeEl.childNodes];\n    while ((nodeToInsert = fakeEl.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            ) {\n                if (isUnbreakable(currentNode.parentElement)) {\n                    makeContentsInline(fakeEl);\n                    nodeToInsert = fakeEl.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                    currentNode = insertBefore ? right : left;\n                } else {\n                    currentNode = currentNode.parentElement;\n                }\n            }\n        }\n        if (insertBefore) {\n            currentNode.before(nodeToInsert);\n            insertBefore = false;\n        } else {\n            currentNode.after(nodeToInsert);\n        }\n        if (currentNode.tagName !== 'BR' && isShrunkBlock(currentNode)) {\n            currentNode.remove();\n        }\n        currentNode = nodeToInsert;\n    }\n\n    currentNode = lastChildNode || currentNode;\n    selection.removeAllRanges();\n    const newRange = new Range();\n    let lastPosition = rightPos(currentNode);\n    if (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 insertedNodes;\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 (isContentTextNode(node) && isVisible(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' && style[mode] !== parent.style[mode]) ||\n        (classRegex.test(element.className) &&\n            getComputedStyle(element)[mode] !== getComputedStyle(parent)[mode])\n    );\n}\nfunction addColumn(editor, beforeOrAfter) {\n    getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td.\n    const c = getInSelection(editor.document, 'td');\n    if (!c) return;\n    const i = [...closestElement(c, 'tr').querySelectorAll('th, td')].findIndex(td => td === c);\n    const column = closestElement(c, 'table').querySelectorAll(`tr td:nth-of-type(${i + 1})`);\n    column.forEach(row => row[beforeOrAfter](document.createElement('td')));\n}\nfunction addRow(editor, beforeOrAfter) {\n    getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr.\n    const row = getInSelection(editor.document, 'tr');\n    if (!row) return;\n    const newRow = document.createElement('tr');\n    const cells = row.querySelectorAll('td');\n    newRow.append(...Array.from(Array(cells.length)).map(() => {\n        const td = document.createElement('td');\n        td.append(document.createElement('br'));\n        return td;\n    }));\n    row[beforeOrAfter](newRow);\n}\nfunction 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\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    // Insertion\n    insertHTML: (editor, data) => {\n        return insert(editor, data, false);\n    },\n    insertText: (editor, data) => {\n        return insert(editor, data);\n    },\n    insertFontAwesome: (editor, faClass = 'fa fa-star') => {\n        const insertedNode = editorCommands.insertHTML(editor, '<i></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) {\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        ));\n        const [startContainer, startOffset, endContainer, endOffset] = [firstLeaf(range.startContainer), range.startOffset, lastLeaf(range.endContainer), range.endOffset];\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                    setTagName(block, tagName);\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 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    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 && n.nodeValue.trim().length);\n\n        const changedElements = [];\n        const defaultDirection = editor.options.direction;\n        const shouldApplyStyle = !isSelectionFormat(editor.editable, 'switchDirection');\n        for (const block of new Set(selectedTextNodes.map(textNode => closestElement(textNode, 'ul,ol') || closestBlock(textNode)))) {\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        editor.document.execCommand('removeFormat');\n        for (const node of getTraversedNodes(editor.editable)) {\n            // The only possible background image on text is the gradient.\n            closestElement(node).style.backgroundImage = '';\n        }\n        textAlignStyles.forEach((textAlign, block) => {\n            block.style.setProperty('text-align', textAlign);\n        });\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    createLink: (editor, link, content) => {\n        const sel = editor.document.getSelection();\n        if (content && !sel.isCollapsed) {\n            editor.deleteRange(sel);\n        }\n        if (sel.isCollapsed) {\n            insertText(sel, content || 'link');\n        }\n        const currentLink = closestElement(sel.focusNode, 'a');\n        link = link || prompt('URL or Email', (currentLink && currentLink.href) || 'http://');\n        const res = editor.document.execCommand('createLink', false, link);\n        if (res) {\n            setSelection(sel.anchorNode, sel.anchorOffset, sel.focusNode, sel.focusOffset);\n            const node = findNode(closestPath(sel.focusNode), node => node.tagName === 'A');\n            for (const [param, value] of Object.entries(editor.options.defaultLinkAttributes)) {\n                node.setAttribute(param, `${value}`);\n            }\n            const pos = [node.parentElement, childNodeIndex(node) + 1];\n            setSelection(...pos, ...pos, false);\n        }\n    },\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));\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            ) {\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            if (node.nodeType === Node.TEXT_NODE && !isVisibleStr(node)) {\n                node.remove();\n            } else {\n                let block = closestBlock(node);\n                if (!['OL', 'UL'].includes(block.tagName)) {\n                    block = block.closest('li') || block;\n                    const ublock = block.closest('ol, ul');\n                    ublock && getListMode(ublock) == mode ? li.add(block) : blocks.add(block);\n                }\n            }\n        }\n\n        let target = [...(blocks.size ? blocks : li)];\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        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) {\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 selectedNodes = getSelectedNodes(editor.editable);\n        if (isEmptyBlock(range.endContainer)) {\n            selectedNodes.push(range.endContainer, ...descendants(range.endContainer));\n        }\n        const fonts = 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 && isVisibleStr(node))\n                    || (isEmptyBlock(node.parentNode))\n                    || (node.nodeType === Node.ELEMENT_NODE &&\n                        ['inline', 'inline-block'].includes(getComputedStyle(node).display) &&\n                        isVisibleStr(node.textContent) &&\n                        !node.classList.contains('btn') &&\n                        !node.querySelector('font'))) {\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        // 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];\n    },\n    // Table\n    insertTable: (editor, { rowNumber = 2, colNumber = 2 } = {}) => {\n        const tdsHtml = new Array(colNumber).fill('<td><br></td>').join('');\n        const trsHtml = new Array(rowNumber).fill(`<tr>${tdsHtml}</tr>`).join('');\n        const tableHtml = `<table class=\"table table-bordered\"><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.insertHTML(editor, tableHtml);\n        setCursorStart(table.querySelector('td'));\n    },\n    addColumnLeft: editor => {\n        addColumn(editor, 'before');\n    },\n    addColumnRight: editor => {\n        addColumn(editor, 'after');\n    },\n    addRowAbove: editor => {\n        addRow(editor, 'before');\n    },\n    addRowBelow: editor => {\n        addRow(editor, 'after');\n    },\n    removeColumn: editor => {\n        getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding td.\n        const cell = getInSelection(editor.document, 'td');\n        if (!cell) return;\n        const table = closestElement(cell, 'table');\n        const cells = [...closestElement(cell, 'tr').querySelectorAll('th, td')];\n        const index = cells.findIndex(td => td === cell);\n        const siblingCell = cells[index - 1] || cells[index + 1];\n        table.querySelectorAll(`tr td:nth-of-type(${index + 1})`).forEach(td => td.remove());\n        siblingCell ? setSelection(...startPos(siblingCell)) : deleteTable(editor, table);\n    },\n    removeRow: editor => {\n        getDeepRange(editor.editable, { select: true }); // Ensure deep range for finding tr.\n        const row = getInSelection(editor.document, 'tr');\n        if (!row) return;\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)) : deleteTable(editor, table);\n    },\n    deleteTable: (editor, table) => deleteTable(editor, table),\n    insertHorizontalRule(editor) {\n        const selection = editor.document.getSelection();\n        const range = selection.getRangeAt(0);\n        const element = closestElement(\n            range.startContainer,\n            'P, PRE, H1, H2, H3, H4, H5, H6, BLOCKQUOTE',\n        );\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 } from '../utils/constants.js';\nimport {\n    boundariesOut,\n    childNodeIndex,\n    CTGROUPS,\n    CTYPES,\n    DIRECTIONS,\n    endPos,\n    fillEmpty,\n    getState,\n    isBlock,\n    isInPre,\n    isUnremovable,\n    isVisible,\n    isVisibleStr,\n    leftPos,\n    rightPos,\n    moveNodes,\n    nodeSize,\n    prepareUpdate,\n    setSelection,\n    splitTextNode,\n    isMediaElement,\n    isVisibleEmpty,\n    isNotEditableNode,\n    createDOMPathGenerator,\n    closestElement,\n} from '../utils/utils.js';\n\nText.prototype.oDeleteBackward = function (offset, alreadyMoved = false) {\n    const parentNode = this.parentNode;\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        HTMLElement.prototype.oDeleteBackward.call(this, offset, alreadyMoved);\n        return;\n    }\n\n    // Get the size of the unicode character to remove.\n    const charSize = [...this.nodeValue.slice(0, offset)].pop().length;\n    // Split around the character where the backspace occurs.\n    const firstSplitOffset = splitTextNode(this, offset - charSize);\n    const secondSplitOffset = splitTextNode(parentNode.childNodes[firstSplitOffset], charSize);\n    const middleNode = parentNode.childNodes[firstSplitOffset];\n\n    // Do remove the character, then restore the state of the surrounding parts.\n    const restore = prepareUpdate(parentNode, firstSplitOffset, parentNode, secondSplitOffset);\n    const isSpace = !isVisibleStr(middleNode) && !isInPre(middleNode);\n    const isZWS = middleNode.nodeValue === '\\u200B';\n    middleNode.remove();\n    restore();\n\n    // If the removed element was not visible content, propagate the backspace.\n    if (\n        isZWS ||\n        isSpace &&\n        getState(parentNode, firstSplitOffset, DIRECTIONS.LEFT).cType !== CTYPES.CONTENT\n    ) {\n        parentNode.oDeleteBackward(firstSplitOffset, alreadyMoved);\n        if (isZWS) {\n            fillEmpty(parentNode);\n        }\n        return;\n    }\n\n    fillEmpty(parentNode);\n    setSelection(parentNode, firstSplitOffset);\n};\n\nconst isDeletable = (node) => {\n    return isMediaElement(node) || isNotEditableNode(node);\n}\n\nHTMLElement.prototype.oDeleteBackward = function (offset, alreadyMoved = false, offsetLimit) {\n    const contentIsZWS = this.textContent === '\\u200B';\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) || isVisibleEmpty(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        const parentEl = this.parentNode;\n        const closestLi = closestElement(this, 'li');\n        if ((closestLi && !closestLi.previousElementSibling) || !isBlock(this) || isVisibleEmpty(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) && !contentIsZWS;\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        /**\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            ['BLOCKQUOTE', 'H1', 'H2', 'H3', 'PRE'].includes(this.nodeName) &&\n            !closestLi\n        ) {\n            const p = document.createElement('p');\n            p.replaceChildren(...this.childNodes);\n            this.replaceWith(p);\n            setSelection(p, offset);\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 the element\n    //  => when Deleting and merging an element the containing node will temporary be hosted\n    //  in the common parent beside possible other nodes. We don't want to touch those others node when merging\n    //  two html elements\n    //  ex : <div>12<p>ab[]</p><p>cd</p>34</div> should never touch the 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 a inline node,\n            // we need to 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>\n            // In this case we cannot simply merge the <p> content into the div parent\n            // or we would loose the 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 (offset > 0 || this.previousElementSibling) {\n        // If backspace inside li content or if the li is not the first one,\n        // it behaves just like in a normal element.\n        HTMLElement.prototype.oDeleteBackward.call(this, offset, alreadyMoved);\n        return;\n    }\n    this.oShiftTab(offset);\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 {\n    findNode,\n    isContentTextNode,\n    isVisibleEmpty,\n    nodeSize,\n    rightPos,\n    getState,\n    DIRECTIONS,\n    CTYPES,\n    leftPos,\n    isFontAwesome,\n    rightLeafOnlyNotBlockNotEditablePath,\n    rightLeafOnlyPathNotBlockNotEditablePath,\n    isNotEditableNode,\n    splitTextNode,\n    prepareUpdate,\n    isVisibleStr,\n    isInPre,\n    fillEmpty,\n    setSelection,\n    isZWS,\n    childNodeIndex, boundariesOut\n} from '../utils/utils.js';\n\nText.prototype.oDeleteForward = function (offset, alreadyMoved = false) {\n    const parentNode = this.parentNode;\n\n    if (offset === this.nodeValue.length) {\n        // Delete at the end of a text node is not a specific case to\n        // handle, let the element implementation handle it.\n        HTMLElement.prototype.oDeleteForward.call(this, offset, alreadyMoved);\n        return;\n    }\n\n    // Get the size of the unicode character to remove.\n    const charSize = [...this.nodeValue.slice(0, offset + 1)].pop().length;\n    // Split around the character where the delete occurs.\n    const firstSplitOffset = splitTextNode(this, offset);\n    const secondSplitOffset = splitTextNode(parentNode.childNodes[firstSplitOffset], charSize);\n    const middleNode = parentNode.childNodes[firstSplitOffset];\n\n    // Do remove the character, then restore the state of the surrounding parts.\n    const restore = prepareUpdate(parentNode, firstSplitOffset, parentNode, secondSplitOffset);\n    const isSpace = !isVisibleStr(middleNode) && !isInPre(middleNode);\n    const isZWS = middleNode.nodeValue === '\\u200B';\n    middleNode.remove();\n    restore();\n\n    // If the removed element was not visible content, propagate the delete.\n    if (\n        isZWS ||\n        (isSpace &&\n        getState(parentNode, firstSplitOffset, DIRECTIONS.RIGHT).cType !== CTYPES.CONTENT)\n    ) {\n        parentNode.oDeleteForward(firstSplitOffset, alreadyMoved);\n        if (isZWS) {\n            fillEmpty(parentNode);\n        }\n        return;\n    }\n    fillEmpty(parentNode);\n    setSelection(parentNode, firstSplitOffset);\n};\n\nHTMLElement.prototype.oDeleteForward = function (offset) {\n    const filterFunc = node =>\n        isVisibleEmpty(node) || isContentTextNode(node) || isNotEditableNode(node);\n\n    const firstLeafNode = findNode(rightLeafOnlyNotBlockNotEditablePath(this, offset), filterFunc);\n    if (firstLeafNode &&\n        isZWS(firstLeafNode) &&\n        this.parentElement.hasAttribute('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    }\n    if (\n        this.hasAttribute &&\n        this.hasAttribute('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 && (isFontAwesome(firstLeafNode) || isNotEditableNode(firstLeafNode))) {\n        firstLeafNode.remove();\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    const firstOutNode = findNode(\n        rightLeafOnlyPathNotBlockNotEditablePath(\n            ...(firstLeafNode ? rightPos(firstLeafNode) : [this, offset]),\n        ),\n        filterFunc,\n    );\n    if (firstOutNode) {\n        const [node, offset] = leftPos(firstOutNode);\n        node.oDeleteBackward(offset);\n        return;\n    }\n};\n", "/** @odoo-module **/\nimport { UNBREAKABLE_ROLLBACK_CODE } from '../utils/constants.js';\n\nimport {\n    childNodeIndex,\n    clearEmpty,\n    fillEmpty,\n    isBlock,\n    isUnbreakable,\n    prepareUpdate,\n    setCursorStart,\n    setCursorEnd,\n    setTagName,\n    splitTextNode,\n    toggleClass,\n    isVisible,\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    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    const 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 (isVisible(splitEl)) {\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        fillEmpty(clearEmpty(this));\n        fillEmpty(splitEl);\n\n        const focusToElement =\n            splitEl.nodeType === Node.ELEMENT_NODE && splitEl.tagName === 'A'\n                ? clearEmpty(splitEl)\n                : splitEl;\n        setCursorStart(focusToElement);\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.textContent) {\n        const node = setTagName(newEl, 'P');\n        setCursorStart(node);\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    if (this.textContent) {\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        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", "/** @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 (li.parentNode.parentNode.tagName === 'LI') {\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                p = isVisible(p) && ul.after(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 (isVisible(p)) ul.after(p);\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    getListMode,\n    isBlock,\n    preserveCursor,\n    setTagName,\n    toggleClass,\n    insertListAfter,\n    getAdjacents,\n} from '../utils/utils.js';\n\nText.prototype.oToggleList = function (offset, mode) {\n    this.parentElement.oToggleList(childNodeIndex(this), mode);\n};\n\nHTMLElement.prototype.oToggleList = function (offset, mode = 'UL') {\n    if (!isBlock(this)) {\n        return this.parentElement.oToggleList(childNodeIndex(this));\n    }\n    const inLI = this.closest('li');\n    if (inLI) {\n        return inLI.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        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        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    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    this.remove();\n\n    restoreCursor(new Map([[this, list.firstChild]]));\n    return true;\n};\n\nHTMLLIElement.prototype.oToggleList = function (offset, mode) {\n    const pnode = this.closest('ul, ol');\n    if (!pnode) return;\n    const restoreCursor = preserveCursor(this.ownerDocument);\n    const listMode = getListMode(pnode) + mode;\n    if (['OLCL', 'ULCL'].includes(listMode)) {\n        pnode.classList.add('o_checklist');\n        for (let li = pnode.firstElementChild; li !== null; li = li.nextElementSibling) {\n            if (li.style.listStyle != 'none') {\n                li.style.listStyle = null;\n                if (!li.style.all) li.removeAttribute('style');\n            }\n        }\n        setTagName(pnode, 'UL');\n    } else if (['CLOL', 'CLUL'].includes(listMode)) {\n        toggleClass(pnode, 'o_checklist');\n        setTagName(pnode, mode);\n    } else if (['OLUL', 'ULOL'].includes(listMode)) {\n        setTagName(pnode, mode);\n    } else {\n        // toggle => remove list\n        let node = this;\n        while (node) {\n            node = node.oShiftTab(offset);\n        }\n    }\n\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 */\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        this._createClient(notification.fromClientId);\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) console.log(`%cisOfferRacing: ${isOfferRacing}`, 'background: red;');\n\n        if (isOfferRacing) {\n            if (debugShowLog)\n                console.log(`%c SETREMOTEDESCRIPTION 1`, 'background: navy; color:white;');\n            await Promise.all([\n                pc.setLocalDescription({ type: 'rollback' }),\n                pc.setRemoteDescription(description),\n            ]);\n        } else {\n            if (debugShowLog)\n                console.log(`%c SETREMOTEDESCRIPTION 2`, 'background: navy; color:white;');\n            await pc.setRemoteDescription(description);\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            await pc.setLocalDescription(answerDescription);\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._stopped = true;\n        this.closeAllConnections();\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        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            const baseMethod = baseNotificationMethods[notification.notificationName];\n            if (baseMethod) {\n                baseMethod.call(this, notification);\n            }\n            if (this.options.onNotification) {\n                this.options.onNotification(notification);\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 rejectTimeout = setTimeout(() => {\n                reject('Request took too long (more than 10 seconds).');\n                delete this._pendingRequestResolver[requestId];\n            }, 10000);\n\n            this._pendingRequestResolver[requestId] = {\n                resolve,\n                rejectTimeout,\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\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 a new transaction:\n     * 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", "odoo.define('wysiwyg.fonts', function (require) {\n'use strict';\n\nreturn {\n    /**\n     * Retrieves all the CSS rules which match the given parser (Regex).\n     *\n     * @param {Regex} filter\n     * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is\n     *          defined by 3 values: 'selector', 'css' and 'names'. 'selector'\n     *          is a string which contains the whole selector, 'css' is a string\n     *          which contains the css properties and 'names' is an array of the\n     *          first captured groups for each selector part. E.g.: if the\n     *          filter is set to match .fa-* rules and capture the icon names,\n     *          the rule:\n     *              '.fa-alias1::before, .fa-alias2::before { hello: world; }'\n     *          will be retrieved as\n     *              {\n     *                  selector: '.fa-alias1::before, .fa-alias2::before',\n     *                  css: 'hello: world;',\n     *                  names: ['.fa-alias1', '.fa-alias2'],\n     *              }\n     */\n    cacheCssSelectors: {},\n    getCssSelectors: function (filter) {\n        if (this.cacheCssSelectors[filter]) {\n            return this.cacheCssSelectors[filter];\n        }\n        this.cacheCssSelectors[filter] = [];\n        var sheets = document.styleSheets;\n        for (var i = 0; i < sheets.length; i++) {\n            var rules;\n            try {\n                // try...catch because Firefox not able to enumerate\n                // document.styleSheets[].cssRules[] for cross-domain\n                // stylesheets.\n                rules = sheets[i].rules || sheets[i].cssRules;\n            } catch (e) {\n                continue;\n            }\n            if (!rules) {\n                continue;\n            }\n\n            for (var r = 0 ; r < rules.length ; r++) {\n                var selectorText = rules[r].selectorText;\n                if (!selectorText) {\n                    continue;\n                }\n                var selectors = selectorText.split(/\\s*,\\s*/);\n                var data = null;\n                for (var s = 0; s < selectors.length; s++) {\n                    var match = selectors[s].trim().match(filter);\n                    if (!match) {\n                        continue;\n                    }\n                    if (!data) {\n                        data = {\n                            selector: match[0],\n                            css: rules[r].cssText.replace(/(^.*\\{\\s*)|(\\s*\\}\\s*$)/g, ''),\n                            names: [match[1]]\n                        };\n                    } else {\n                        data.selector += (', ' + match[0]);\n                        data.names.push(match[1]);\n                    }\n                }\n                if (data) {\n                    this.cacheCssSelectors[filter].push(data);\n                }\n            }\n        }\n        return this.cacheCssSelectors[filter];\n    },\n    /**\n     * List of font icons to load by editor. The icons are displayed in the media\n     * editor and identified like font and image (can be colored, spinned, resized\n     * with fa classes).\n     * To add font, push a new object {base, parser}\n     *\n     * - base: class who appear on all fonts\n     * - parser: regular expression used to select all font in css stylesheets\n     *\n     * @type Array\n     */\n    fontIcons: [{base: 'fa', parser: /\\.(fa-(?:\\w|-)+)::?before/i}],\n    /**\n     * Searches the fonts described by the @see fontIcons variable.\n     */\n    computeFonts: _.once(function () {\n        var self = this;\n        _.each(this.fontIcons, function (data) {\n            data.cssData = self.getCssSelectors(data.parser);\n            data.alias = _.flatten(_.map(data.cssData, _.property('names')));\n        });\n    }),\n};\n});\n", "odoo.define('web_editor.base', function (require) {\n'use strict';\n\n// TODO this should be re-removed as soon as possible.\n\nvar ajax = require('web.ajax');\nvar session = require('web.session');\n\nvar domReady = new Promise(function(resolve) {\n    $(resolve);\n});\n\nreturn {\n    /**\n     * Retrieves all the CSS rules which match the given parser (Regex).\n     *\n     * @param {Regex} filter\n     * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is\n     *          defined by 3 values: 'selector', 'css' and 'names'. 'selector'\n     *          is a string which contains the whole selector, 'css' is a string\n     *          which contains the css properties and 'names' is an array of the\n     *          first captured groups for each selector part. E.g.: if the\n     *          filter is set to match .fa-* rules and capture the icon names,\n     *          the rule:\n     *              '.fa-alias1::before, .fa-alias2::before { hello: world; }'\n     *          will be retrieved as\n     *              {\n     *                  selector: '.fa-alias1::before, .fa-alias2::before',\n     *                  css: 'hello: world;',\n     *                  names: ['.fa-alias1', '.fa-alias2'],\n     *              }\n     */\n    cacheCssSelectors: {},\n    getCssSelectors: function (filter) {\n        if (this.cacheCssSelectors[filter]) {\n            return this.cacheCssSelectors[filter];\n        }\n        this.cacheCssSelectors[filter] = [];\n        var sheets = document.styleSheets;\n        for (var i = 0; i < sheets.length; i++) {\n            var rules;\n            try {\n                // try...catch because Firefox not able to enumerate\n                // document.styleSheets[].cssRules[] for cross-domain\n                // stylesheets.\n                rules = sheets[i].rules || sheets[i].cssRules;\n            } catch (e) {\n                console.warn(\"Can't read the css rules of: \" + sheets[i].href, e);\n                continue;\n            }\n            if (!rules) {\n                continue;\n            }\n\n            for (var r = 0 ; r < rules.length ; r++) {\n                var selectorText = rules[r].selectorText;\n                if (!selectorText) {\n                    continue;\n                }\n                var selectors = selectorText.split(/\\s*,\\s*/);\n                var data = null;\n                for (var s = 0; s < selectors.length; s++) {\n                    var match = selectors[s].trim().match(filter);\n                    if (!match) {\n                        continue;\n                    }\n                    if (!data) {\n                        data = {\n                            selector: match[0],\n                            css: rules[r].cssText.replace(/(^.*\\{\\s*)|(\\s*\\}\\s*$)/g, ''),\n                            names: [match[1]]\n                        };\n                    } else {\n                        data.selector += (', ' + match[0]);\n                        data.names.push(match[1]);\n                    }\n                }\n                if (data) {\n                    this.cacheCssSelectors[filter].push(data);\n                }\n            }\n        }\n        return this.cacheCssSelectors[filter];\n    },\n    /**\n     * List of font icons to load by editor. The icons are displayed in the media\n     * editor and identified like font and image (can be colored, spinned, resized\n     * with fa classes).\n     * To add font, push a new object {base, parser}\n     *\n     * - base: class who appear on all fonts\n     * - parser: regular expression used to select all font in css stylesheets\n     *\n     * @type Array\n     */\n    fontIcons: [{base: 'fa', parser: /\\.(fa-(?:\\w|-)+)::?before/i}],\n    /**\n     * Searches the fonts described by the @see fontIcons variable.\n     */\n    computeFonts: _.once(function () {\n        var self = this;\n        _.each(this.fontIcons, function (data) {\n            data.cssData = self.getCssSelectors(data.parser);\n            data.alias = _.flatten(_.map(data.cssData, _.property('names')));\n        });\n    }),\n    /**\n     * If a widget needs to be instantiated on page loading, it needs to wait\n     * for appropriate resources to be loaded. This function returns a Promise\n     * which is resolved when the dom is ready, the session is bound\n     * (translations loaded) and the XML is loaded. This should however not be\n     * necessary anymore as widgets should not be parentless and should then be\n     * instantiated (directly or not) by the page main component (webclient,\n     * website root, editor bar, ...). The DOM will be ready then, the main\n     * component is in charge of waiting for the session and the XML can be\n     * lazy loaded thanks to the @see Widget.xmlDependencies key.\n     *\n     * @returns {Promise}\n     */\n    ready: function () {\n        return Promise.all([domReady, session.is_bound, ajax.loadXML()]);\n    },\n};\n});\n\n//==============================================================================\n\nodoo.define('web_editor.context', function (require) {\n'use strict';\n\n// TODO this should be re-removed as soon as possible.\n\nfunction getContext(context) {\n    var html = document.documentElement;\n    return _.extend({\n        lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'),\n\n        // Unfortunately this is a mention of 'website' in 'web_editor' as there\n        // was no other way to do it as this was restored in a stable version.\n        // Indeed, the editor is currently using this context at the root of JS\n        // module, so there is no way for website to hook itself before\n        // web_editor uses it (without a risky refactoring of web_editor in\n        // stable). As mentioned above, the editor should not use this context\n        // anymore anyway (this was restored by the saas-12.2 editor revert).\n        'website_id': html.getAttribute('data-website-id') | 0,\n    }, context || {});\n}\nfunction getExtraContext(context) {\n    var html = document.documentElement;\n    return _.extend(getContext(), {\n        editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python\n        translatable: !!html.dataset.translatable,\n        edit_translations: !!html.dataset.edit_translations,\n    }, context || {});\n}\n\nreturn {\n    get: getContext,\n    getExtra: getExtraContext,\n};\n});\n\n//==============================================================================\n\nodoo.define('web_editor.ready', function (require) {\n'use strict';\n\n// TODO this should be re-removed as soon as possible.\n\nvar base = require('web_editor.base');\n\nreturn base.ready();\n});\n", "odoo.define('web_editor.image_processing', function (require) {\n'use strict';\n\n// Fields returned by cropperjs 'getData' method, also need to be passed when\n// initializing the cropper to reuse the previous crop.\nconst 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];\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 */\nasync function applyModifications(img, dataOptions = {}) {\n    const data = Object.assign({\n        glFilter: '',\n        filter: '#0000',\n        quality: '75',\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    } = data;\n    [width, height, resizeWidth] = [width, height, resizeWidth].map(s => parseFloat(s));\n    quality = parseInt(quality);\n\n    // Crop\n    const container = document.createElement('div');\n    const original = await loadImage(originalSrc);\n    container.appendChild(original);\n    await activateCropper(original, 0, data);\n    const croppedImg = $(original).cropper('getCroppedCanvas', {width, height});\n    $(original).cropper('destroy');\n\n    // Width\n    const result = document.createElement('canvas');\n    result.width = resizeWidth || croppedImg.width;\n    result.height = 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    ctx.drawImage(croppedImg, 0, 0, croppedImg.width, croppedImg.height, 0, 0, result.width, result.height);\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    return result.toDataURL(mimetype, quality / 100);\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 */\nfunction loadImage(src, img = new Image()) {\n    return new Promise((resolve, reject) => {\n        img.addEventListener('load', () => resolve(img), {once: true});\n        img.addEventListener('error', reject, {once: true});\n        img.src = src;\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 * 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 */\nasync function activateCropper(image, aspectRatio, dataset) {\n    const src = image.getAttribute('src');\n    if (!imageCache.has(src)) {\n        const res = await fetch(src);\n        imageCache.set(src, URL.createObjectURL(await res.blob()));\n    }\n    image.src = imageCache.get(src);\n    $(image).cropper({\n        viewMode: 2,\n        dragMode: 'move',\n        autoCropArea: 1.0,\n        aspectRatio: aspectRatio,\n        data: _.mapObject(_.pick(dataset, ...cropperDataFields), value => 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    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 {Function} rpc a function that can be used to make the RPC. Typically\n *   this would be passed as 'this._rpc.bind(this)' from widgets.\n * @param {string} [attachmentSrc=''] specifies the URL of the corresponding\n * attachment if it can't be found in the 'src' attribute.\n */\nasync function loadImageInfo(img, rpc, attachmentSrc = '') {\n    const src = attachmentSrc || img.getAttribute('src');\n    // If there is a marked originalSrc, the data is already loaded.\n    if (img.dataset.originalSrc || !src) {\n        return;\n    }\n\n    const {original} = await rpc({\n        route: '/web_editor/get_image_info',\n        params: {src: src.split(/[?#]/)[0]},\n    });\n    // Check that url is local.\n    const isLocal = original && new URL(original.image_src, window.location.origin).origin === window.location.origin;\n    if (isLocal && original.image_src) {\n        img.dataset.originalId = original.id;\n        img.dataset.originalSrc = original.image_src;\n        img.dataset.mimetype = original.mimetype;\n    }\n}\n\n/**\n * @param {String} mimetype\n * @returns {Boolean}\n */\nfunction isImageSupportedForProcessing(mimetype) {\n    return ['image/jpeg', 'image/png'].includes(mimetype);\n}\n/**\n * @param {HTMLImageElement} img\n * @returns {Boolean}\n */\nfunction isImageSupportedForStyle(img) {\n    return img.parentElement && !img.parentElement.dataset.oeType\n        // Editable root elements are technically *potentially* supported here\n        // (if the edited attributes are not computed inside the related view,\n        // they could technically be saved... but as we cannot tell the computed\n        // ones apart from the \"static\" ones, we choose to not support edition\n        // at all in those \"root\" cases).\n        && !img.dataset.oeXpath;\n}\n\nreturn {\n    applyModifications,\n    cropperDataFields,\n    activateCropper,\n    loadImageInfo,\n    loadImage,\n    removeOnImageChangeAttrs: [...cropperDataFields, ...modifierFields, 'aspectRatio'],\n    isImageSupportedForProcessing,\n    isImageSupportedForStyle,\n};\n});\n", "\nodoo.define('web_editor.custom_colors', function (require) {\n'use strict';\n\n// These colors are already normalized as per normalizeCSSColor in web.Colorpicker\nreturn [\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});\n", "odoo.define('wysiwyg.widgets.AltDialog', function (require) {\n'use strict';\n\nvar core = require('web.core');\nvar Dialog = require('wysiwyg.widgets.Dialog');\n\nvar _t = core._t;\n\n/**\n * Let users change the alt & title of a media.\n */\nvar AltDialog = Dialog.extend({\n    template: 'wysiwyg.widgets.alt',\n    xmlDependencies: Dialog.prototype.xmlDependencies.concat(\n        ['/web_editor/static/src/xml/wysiwyg.xml']\n    ),\n\n    /**\n     * @constructor\n     */\n    init: function (parent, options, media) {\n        options = options || {};\n        this._super(parent, _.extend({}, {\n            title: _t(\"Change media description and tooltip\")\n        }, options));\n\n        this.media = media;\n        var allEscQuots = /&quot;/g;\n        this.alt = ($(this.media).attr('alt') || \"\").replace(allEscQuots, '\"');\n        var title = $(this.media).attr('title') || $(this.media).data('original-title') || \"\";\n        this.tag_title = (title).replace(allEscQuots, '\"');\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    save: function () {\n        var alt = this.$('#alt').val();\n        var title = this.$('#title').val();\n        var allNonEscQuots = /\"/g;\n        $(this.media).attr('alt', alt ? alt.replace(allNonEscQuots, \"&quot;\") : null)\n            .attr('title', title ? title.replace(allNonEscQuots, \"&quot;\") : null);\n        $(this.media).trigger('content_changed');\n        this.final_data = this.media;\n        return this._super.apply(this, arguments);\n    },\n});\n\n\nreturn AltDialog;\n});\n", "odoo.define('web_editor.ColorPalette', function (require) {\n'use strict';\n\nconst ajax = require('web.ajax');\nconst core = require('web.core');\nconst session = require('web.session');\nconst {ColorpickerWidget} = require('web.Colorpicker');\nconst Widget = require('web.Widget');\nconst customColors = require('web_editor.custom_colors');\nconst weUtils = require('web_editor.utils');\n\nconst qweb = core.qweb;\n\nlet colorpickerArch;\n\nconst ColorPaletteWidget = Widget.extend({\n    // ! for xmlDependencies, see loadDependencies function\n    template: 'web_editor.snippet.option.colorpicker',\n    events: {\n        'click .o_we_color_btn': '_onColorButtonClick',\n        'mouseenter .o_we_color_btn': '_onColorButtonEnter',\n        'mouseleave .o_we_color_btn': '_onColorButtonLeave',\n        'click .o_we_colorpicker_switch_pane_btn': '_onSwitchPaneButtonClick',\n        'click .o_custom_gradient_editor .o_custom_gradient_btn': '_onGradientCustomButtonClick',\n        'click .o_custom_gradient_editor': '_onPanelClick',\n        'change .o_custom_gradient_editor input[type=\"text\"]': '_onGradientInputChange',\n        'keypress .o_custom_gradient_editor input[type=\"text\"]': '_onGradientInputKeyPress',\n        'click .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonClick',\n        'mouseenter .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonEnter',\n        'mouseleave .o_custom_gradient_editor we-button:not(.o_remove_color)': '_onGradientButtonLeave',\n        'click .o_custom_gradient_scale': '_onGradientPreviewClick',\n        // Note: _onGradientSliderClick on slider is attached at slider creation.\n        'click .o_custom_gradient_editor .o_remove_color': '_onGradientDeleteClick',\n    },\n    custom_events: {\n        'colorpicker_select': '_onColorPickerSelect',\n        'colorpicker_preview': '_onColorPickerPreview',\n    },\n    /**\n     * @override\n     *\n     * @param {Object} [options]\n     * @param {string} [options.selectedColor] The class or css attribute color selected by default.\n     * @param {boolean} [options.resetButton=true] Whether to display or not the reset button.\n     * @param {string[]} [options.excluded=[]] Sections not to display.\n     * @param {string[]} [options.excludeSectionOf] Extra section to exclude: the one containing the named color.\n     * @param {boolean} [options.withCombinations=false] Enable color combinations selection.\n     * @param {float} [options.noTransparency=false] Specify a default opacity (predefined gradients & color).\n     * @param {float} [options.opacity=1] Specify a default opacity (predefined gradients & color).\n     * @param {string} [options.selectedTab='theme-colors'] Tab initially selected.\n     * @param {boolean} [options.withGradients=false] Enable gradient selection.\n     * @param {JQuery} [options.$editable=$()] Editable content from which the custom colors are retrieved.\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.style = window.getComputedStyle(document.documentElement);\n        this.options = _.extend({\n            selectedColor: false,\n            resetButton: true,\n            excluded: [],\n            excludeSectionOf: null,\n            $editable: $(),\n            withCombinations: false,\n            noTransparency: false,\n            opacity: 1,\n            selectedTab: 'theme-colors',\n            withGradients: false,\n        }, options || {});\n        this.selectedColor = '';\n        this.resetButton = this.options.resetButton;\n        this.withCombinations = this.options.withCombinations;\n\n        this.trigger_up('request_editable', {callback: val => this.options.$editable = val});\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.options.withGradients ? [\n                'predefined_gradients',\n                'custom_gradient',\n            ] : [],\n        }];\n\n        this.sections = {};\n        this.pickers = {};\n    },\n    /**\n     * @override\n     */\n    willStart: async function () {\n        await this._super(...arguments);\n        await ColorPaletteWidget.loadDependencies(this);\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        const res = this._super.apply(this, arguments);\n\n        const switchPaneButtons = this.el.querySelectorAll('.o_we_colorpicker_switch_pane_btn');\n\n        let colorpickerEl;\n        if (colorpickerArch) {\n            colorpickerEl = $(colorpickerArch)[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        _.each(this.tabs, (tab, index) => {\n            // Append pickers to section\n            const sectionEl = this.el.querySelector(`.o_colorpicker_sections[data-color-tab=\"${tab.id}\"]`);\n            let sectionIsEmpty = true;\n            _.each(tab.pickers, 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.options.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.options.selectedTab === tab.id) {\n                    this.options.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.options.withGradients && this.options.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.options.opacity})`);\n                elem.dataset.color = gradient.replaceAll(/\\s+/g, '');\n            });\n        }\n\n        // Palette for gradient\n        if (this.pickers['custom_gradient']) {\n            this.gradientColorPicker = new ColorpickerWidget(this, {\n                stopClickPropagation: true,\n            });\n            await this.gradientColorPicker.appendTo(this.sections['gradients']);\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.options.selectedColor) && this.options.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.options.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        _.each(this.options.excluded, exc => {\n            this.$('[data-name=\"' + exc + '\"]').addClass('d-none');\n        });\n        if (this.options.excludeSectionOf) {\n            this.$('[data-name]:has([data-color=\"' + this.options.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.options.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        const compatibilityColorNames = ['primary', 'secondary', 'alpha', 'beta', 'gamma', 'delta', 'epsilon', 'success', 'info', 'warning', 'danger'];\n        this.colorNames = [...compatibilityColorNames];\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 {\n                $color.addClass(`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        // 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.options.selectedCC) {\n            this.selectedCC = this.options.selectedCC;\n        }\n        if (this.options.selectedColor) {\n            let selectedColor = this.options.selectedColor;\n            if (compatibilityColorNames.includes(selectedColor)) {\n                selectedColor = weUtils.getCSSVariableValue(selectedColor, this.style) || selectedColor;\n            }\n            selectedColor = ColorpickerWidget.normalizeCSSColor(selectedColor);\n            if (selectedColor !== 'rgba(0, 0, 0, 0)') {\n                this.selectedColor = this.colorToColorNames[selectedColor] || selectedColor;\n            }\n        }\n        this._buildCustomColors();\n        this._markSelectedColor();\n\n        // Colorpicker\n        if (!this.options.excluded.includes('custom')) {\n            let defaultColor = this.selectedColor;\n            if (defaultColor && !ColorpickerWidget.isCSSColor(defaultColor)) {\n                defaultColor = weUtils.getCSSVariableValue(defaultColor, this.style);\n            }\n            if (!defaultColor && this.options.opacity !== 1) {\n                defaultColor = 'rgba(0, 0, 0, ' + this.options.opacity + ')';\n            }\n            this.colorPicker = new ColorpickerWidget(this, {\n                defaultColor: defaultColor,\n                noTransparency: !!this.options.noTransparency,\n            });\n            await this.colorPicker.appendTo(this.sections['custom-colors']);\n        }\n        return res;\n    },\n    /**\n     * Return a list of the color names used in the color palette\n     */\n    getColorNames: function () {\n        return this.colorNames;\n    },\n    /**\n     * Gets the currently selected colors.\n     *\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     * Sets the currently selected colors.\n     *\n     * Note: the tab selection is done here because of an optimization to avoid creating the whole\n     * palette hundreds of times when opening the THEME tab.\n     *\n     * @param {string|number} ccValue\n     * @param {string} color rgb[a]\n     * @param {boolean} [selectTab=true]\n     */\n    setSelectedColor: function (ccValue, color, selectTab = true) {\n        if (color === 'rgba(0, 0, 0, 0)' && this.options.opacity !== 1) {\n            color = 'rgba(0, 0, 0, ' + this.options.opacity + ')';\n        }\n        this._selectColor({\n            ccValue: ccValue,\n            color: color,\n        });\n        if (selectTab) {\n            // This is called on open, restore default tab selection\n            const selectedButtonIndex = this.tabs.map(tab => tab.id).indexOf(this.options.selectedTab);\n            this._selectTabFromButton(this.el.querySelectorAll('button')[selectedButtonIndex]);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _buildCustomColors: function () {\n        if (this.options.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        this.trigger_up('get_custom_colors', {\n            onSuccess: (colors) => {\n                colors.forEach(color => {\n                    this._addCustomColor(existingColors, color);\n                });\n            },\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 (ColorpickerWidget.isCSSColor(color)) {\n                this._addCustomColor(existingColors, color);\n            }\n        });\n        _.each(this.options.$editable.find('[style*=\"color\"]'), el => {\n            for (const colorProp of ['color', 'backgroundColor']) {\n                this._addCustomColor(existingColors, el.style[colorProp]);\n            }\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: function (existingColors, color) {\n        if (!color) {\n            return;\n        }\n        if (!ColorpickerWidget.isCSSColor(color)) {\n            color = weUtils.getCSSVariableValue(color, this.style);\n        }\n        const normColor = ColorpickerWidget.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: function (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: function (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: function (buttonEl) {\n        const bgColor = buttonEl.style.backgroundColor;\n        const value = buttonEl.dataset.color || (bgColor && bgColor !== 'initial' ? ColorpickerWidget.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 {string} [eventName]\n     */\n    _selectColor: function (colorInfo, eventName) {\n        this.selectedCC = colorInfo.ccValue;\n        this.selectedColor = colorInfo.color = this.colorToColorNames[colorInfo.color] || colorInfo.color;\n        if (eventName) {\n            this.trigger_up(eventName, colorInfo);\n        }\n        this._buildCustomColors();\n        if (this.colorPicker) {\n            this.colorPicker.setSelectedColor(colorInfo.color);\n        }\n        if (this.gradientColorPicker) {\n            const customGradient = weUtils.isColorGradient(colorInfo.color) ? colorInfo.color : false;\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: function (gradient) {\n        const editor = this.gradientEditorParts;\n        if (this.gradientColorPicker.$el) {\n            this.gradientColorPicker.do_hide();\n        }\n        const colorSplits = [];\n        if (gradient) {\n            gradient = gradient.toLowerCase();\n            // Extract colors and their positions: colors can either be in the #rrggbb format or in the\n            // rgb/rgba(...) format, positions are expected to be expressed as percentages\n            // (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*\\))\\s*([[0-9]+%]?)/g)) {\n                colorSplits.push([entry[1], entry[2].replace('%', '')]);\n            }\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            editor.customButton.dataset.color = false;\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: function (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: function (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: function (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: function ($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        // Note: show before marking the selected color as, unfortunately,\n        // setting the color and updating the UI accordinly relies on the widget\n        // already being rendered in the DOM.\n        this.gradientColorPicker.do_show();\n        this.gradientColorPicker.setSelectedColor(color);\n        this._sortGradientSliders();\n        this._adjustActiveSliderDelete();\n    },\n    /**\n     * Adjusts the position of the slider delete button.\n     *\n     * @private\n     */\n    _adjustActiveSliderDelete: function () {\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: function () {\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: function () {\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 = ColorpickerWidget.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: function (isPreview) {\n        const gradient = this._computeGradient();\n        // Avoid updating an unchanged gradient.\n        if (weUtils.areCssValuesEqual(gradient, this.selectedColor) && !isPreview) {\n            return;\n        }\n        this.trigger_up(isPreview ? 'color_hover' : 'color_picked', Object.assign(this.getSelectedColors(), {\n            color: gradient,\n            target: this.colorPicker.el,\n        }));\n    },\n    /**\n     * Marks the selected colors.\n     *\n     * @private\n     */\n    _markSelectedColor: function () {\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            buttonEl.classList.toggle('selected', value\n                && (this.selectedCC === value || weUtils.areCssValuesEqual(this.selectedColor, value)));\n        }\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    },\n    /**\n     * Updates a gradient color from a selection in the color picker.\n     *\n     * @private\n     * @param {Event} ev from gradient's colorpicker\n     * @param {boolean} isPreview\n     */\n    _updateGradientColor(ev, isPreview) {\n        ev.stopPropagation();\n        const $slider = $(this.gradientEditorParts.sliders).find('input.active');\n        const color = ev.data.cssColor;\n        if (!weUtils.areCssValuesEqual(color, $slider.data('color'))) {\n            const previousColor = $slider.data('color');\n            $slider.data('color', color);\n            this._updateGradient(isPreview);\n            if (isPreview) {\n                $slider.data('color', previousColor);\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: function (ev) {\n        const buttonEl = ev.currentTarget;\n        const colorInfo = Object.assign(this.getSelectedColors(), this._getButtonInfo(buttonEl));\n        this._selectColor(colorInfo, 'color_picked');\n    },\n    /**\n     * Called when a color button is entered.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorButtonEnter: function (ev) {\n        ev.stopPropagation();\n        this.trigger_up('color_hover', Object.assign(this.getSelectedColors(), this._getButtonInfo(ev.currentTarget)));\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: function (ev) {\n        ev.stopPropagation();\n        this.trigger_up('color_leave', Object.assign(this.getSelectedColors(), {\n            target: ev.target,\n        }));\n    },\n    /**\n     * Called when an update is made on the colorpicker.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorPickerPreview: function (ev) {\n        if (ev.target === this.gradientColorPicker) {\n            this._updateGradientColor(ev, true);\n        } else {\n            this.trigger_up('color_hover', Object.assign(this.getSelectedColors(), {\n                color: ev.data.cssColor,\n                target: this.colorPicker.el,\n            }));\n        }\n    },\n    /**\n     * Called when a color is selected on the colorpicker (mouseup).\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorPickerSelect: function (ev) {\n        if (ev.target === this.gradientColorPicker) {\n            this._updateGradientColor(ev);\n        } else {\n            this._selectColor(Object.assign(this.getSelectedColors(), {\n                color: ev.data.cssColor,\n                target: this.colorPicker.el,\n            }), 'custom_color_picked');\n        }\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 = ColorpickerWidget.convertCSSColorToRgba(previousColor);\n            nextColor = ColorpickerWidget.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: function (ev) {\n        if (ev.charCode === $.ui.keyCode.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.trigger_up('color_leave', Object.assign(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(Object.assign(this.getSelectedColors(), {\n            color: gradient,\n            target: this.gradientEditorParts.customButton,\n        }), 'custom_color_picked');\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\n//------------------------------------------------------------------------------\n// Static\n//------------------------------------------------------------------------------\n\n/**\n * Load ColorPaletteWidget dependencies. This allows to load them without\n * instantiating the widget itself.\n *\n * @static\n */\nlet colorpickerTemplateProm;\nColorPaletteWidget.loadDependencies = async function (rpcCapableObj) {\n    const proms = [ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb)];\n\n    // Public user using the editor may have a colorpalette but with\n    // the default wysiwyg ones.\n    if (!session.is_website_user) {\n        // We can call the colorPalette multiple times but only need 1 rpc\n        if (!colorpickerTemplateProm && !colorpickerArch) {\n            colorpickerTemplateProm = rpcCapableObj._rpc({\n                model: 'ir.ui.view',\n                method: 'render_public_asset',\n                args: ['web_editor.colorpicker', {}],\n            }).then(arch => colorpickerArch = arch);\n        }\n        proms.push(colorpickerTemplateProm);\n    }\n\n    return Promise.all(proms);\n};\n\nreturn {\n    ColorPaletteWidget: ColorPaletteWidget,\n};\n});\n", "odoo.define('wysiwyg.widgets.ImageCropWidget', function (require) {\n'use strict';\n\nconst core = require('web.core');\nconst Widget = require('web.Widget');\nconst {applyModifications, cropperDataFields, activateCropper, loadImage, loadImageInfo} = require('web_editor.image_processing');\nconst { Markup } = require('web.utils');\n\nconst _t = core._t;\n\nconst ImageCropWidget = Widget.extend({\n    template: ['wysiwyg.widgets.crop'],\n    xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],\n    events: {\n        'click.crop_options [data-action]': '_onCropOptionClick',\n        // zoom event is triggered by the cropperjs library when the user zooms.\n        'zoom': '_onCropZoom',\n    },\n\n    /**\n     * @constructor\n     */\n    init(parent, media) {\n        this._super(...arguments);\n        this.media = media;\n        this.parent = parent;\n        this.$media = $(media);\n        // Needed for editors in iframes.\n        this.document = media.ownerDocument;\n        // key: ratio identifier, label: displayed to user, value: used by cropper lib\n        this.aspectRatios = {\n            \"0/0\": {label: _t(\"Flexible\"), value: 0},\n            \"16/9\": {label: \"16:9\", value: 16 / 9},\n            \"4/3\": {label: \"4:3\", value: 4 / 3},\n            \"1/1\": {label: \"1:1\", value: 1},\n            \"2/3\": {label: \"2:3\", value: 2 / 3},\n        };\n        const src = this.media.getAttribute('src');\n        const data = Object.assign({}, media.dataset);\n        this.initialSrc = src;\n        this.aspectRatio = data.aspectRatio || \"0/0\";\n        this.mimetype = data.mimetype || src.endsWith('.png') ? 'image/png' : 'image/jpeg';\n    },\n    /**\n     * @override\n     */\n    async willStart() {\n        await this._super.apply(this, arguments);\n        await loadImageInfo(this.media, this._rpc.bind(this));\n        const isIllustration = /^\\/web_editor\\/shape\\/illustration\\//.test(this.media.dataset.originalSrc);\n        if (this.media.dataset.originalSrc && !isIllustration) {\n            this.originalSrc = this.media.dataset.originalSrc;\n            this.originalId = this.media.dataset.originalId;\n            const sel = this.parent.odooEditor && this.parent.odooEditor.document.getSelection();\n            sel && sel.removeAllRanges();\n            return;\n        }\n        // Couldn't find an attachment: not croppable.\n        this.uncroppable = true;\n    },\n    /**\n     * @override\n     */\n    async start() {\n        if (this.uncroppable) {\n            this.displayNotification({\n              type: 'warning',\n              title: _t(\"This image is an external image\"),\n              message: 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            return this.destroy();\n        }\n        const _super = this._super.bind(this);\n        const $cropperWrapper = this.$('.o_we_cropper_wrapper');\n\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        // Overlaying the cropper image over the real image\n        const offset = this.$media.offset();\n        offset.left += parseInt(this.$media.css('padding-left'));\n        offset.top += parseInt(this.$media.css('padding-right'));\n        $cropperWrapper.offset(offset);\n\n        await loadImage(this.originalSrc, cropperImage);\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        this.document.addEventListener('mousedown', this._onDocumentMousedown, {capture: true});\n        this.document.addEventListener('keydown', this._onDocumentKeydown, {capture: true});\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        if (this.$cropperImage) {\n            this.$cropperImage.cropper('destroy');\n            this.document.removeEventListener('mousedown', this._onDocumentMousedown, {capture: true});\n            this.document.removeEventListener('keydown', this._onDocumentKeydown, {capture: true});\n        }\n        this.media.setAttribute('src', this.initialSrc);\n        this.$media.trigger('image_cropper_destroyed');\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\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(false);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Updates the DOM image with cropped data and associates required\n     * information for a potential future save (where required cropped data\n     * attachments will be created).\n     *\n     * @private\n     * @param {boolean} [cropped=true]\n     */\n    async _save(cropped = true) {\n        // Mark the media for later creation of cropped attachment\n        this.media.classList.add('o_modified_image_to_save');\n\n        [...cropperDataFields, 'aspectRatio'].forEach(attr => {\n            delete this.media.dataset[attr];\n            const value = this._getAttributeValue(attr);\n            if (value) {\n                this.media.dataset[attr] = value;\n            }\n        });\n        delete this.media.dataset.resizeWidth;\n        this.initialSrc = await applyModifications(this.media);\n        this.media.classList.toggle('o_we_image_cropped', cropped);\n        this.$media.trigger('image_cropped');\n        this.destroy();\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    //--------------------------------------------------------------------------\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.destroy();\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 (document.body.contains(ev.target) && this.$(ev.target).length === 0) {\n            return this.destroy();\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     * Save crop if user hits enter.\n     * @private\n     * @param {KeyboardEvent} ev\n     */\n    _onDocumentKeydown(ev) {\n        if(ev.key === 'Enter') {\n            return this._save();\n        }\n    }\n});\n\nreturn ImageCropWidget;\n});\n", "odoo.define('wysiwyg.widgets.Link', function (require) {\n'use strict';\n\nconst core = require('web.core');\nconst OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor');\nconst Widget = require('web.Widget');\nconst {isColorGradient} = require('web_editor.utils');\n\nconst getDeepRange = OdooEditorLib.getDeepRange;\nconst getInSelection = OdooEditorLib.getInSelection;\nconst _t = core._t;\n\n/**\n * Allows to customize link content and style.\n */\nconst Link = Widget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],\n    events: {\n        'input': '_onAnyChange',\n        'change': '_onAnyChange',\n        'input input[name=\"url\"]': '__onURLInput',\n        'change input[name=\"url\"]': '_onURLInputChange',\n    },\n\n    /**\n     * @constructor\n     * @param {Boolean} data.isButton - whether if the target is a button element.\n     */\n    init: function (parent, options, editable, data, $button, link) {\n        this.options = options || {};\n        this._super(parent, _.extend({\n            title: _t(\"Link to\"),\n        }, this.options));\n\n        this._setLinkContent = true;\n\n        this.data = data || {};\n        this.isButton = this.data.isButton;\n        this.$button = $button;\n        this.noFocusUrl = this.options.noFocusUrl;\n\n        this.data.className = this.data.className || \"\";\n        this.data.iniClassName = this.data.iniClassName || \"\";\n        this.needLabel = this.data.needLabel || false;\n\n        // Using explicit type 'link' to preserve style when the target is <button class=\"...btn-link\"/>.\n        this.colorsData = [\n            {type: this.isButton ? 'link' : '', label: _t(\"Link\"), btnPreview: 'link'},\n            {type: 'primary', label: _t(\"Primary\"), btnPreview: 'primary'},\n            {type: 'secondary', label: _t(\"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\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 = editable;\n        this.$editable = $(editable);\n\n        if (link) {\n            const range = document.createRange();\n            range.selectNodeContents(link);\n            this.data.range = range;\n            this.$link = $(link);\n            this.linkEl = link;\n        }\n\n        if (this.data.range) {\n            this.$link = this.$link || $(OdooEditorLib.getInSelection(this.editable.ownerDocument, 'a'));\n            this.linkEl = this.$link[0];\n            this.data.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.data.range.cloneContents();\n            const linkText = linkNode.textContent;\n            this.data.content = linkText.replace(/[ \\t\\r\\n]+/g, ' ');\n            this.data.originalText = this.data.content;\n            if (linkNode instanceof DocumentFragment) {\n                this.data.originalHTML = $('<fakeEl>').append(linkNode).html();\n            } else {\n                this.data.originalHTML = linkNode.innerHTML;\n            }\n            this.data.url = this.$link.attr('href') || '';\n        } else {\n            this.data.content = this.data.content ? this.data.content.replace(/[ \\t\\r\\n]+/g, ' ') : '';\n        }\n\n        if (!this.data.url) {\n            const urls = this.data.content.match(OdooEditorLib.URL_REGEX_WITH_INFOS);\n            if (urls) {\n                this.data.url = urls[0];\n            }\n        }\n\n        if (this.linkEl) {\n            this.data.isNewWindow = this.data.isNewWindow || this.linkEl.target === '_blank';\n        }\n\n        const classesToKeep = [\n            'btn-block', 'text-wrap', 'text-nowrap', 'text-left', \n            'text-center', 'text-right', 'text-justify', 'text-truncate',\n        ];\n        const keptClasses = this.data.iniClassName.split(' ').filter(className => classesToKeep.includes(className));\n        const allBtnColorPrefixes = /(^|\\s+)(bg|text|border)(-[a-z0-9_-]*)?/gi;\n        const allBtnClassSuffixes = /(^|\\s+)btn(-[a-z0-9_-]*)?/gi;\n        const allBtnShapes = /\\s*(rounded-circle|flat)\\s*/gi;\n        this.data.className = this.data.iniClassName\n            .replace(allBtnColorPrefixes, ' ')\n            .replace(allBtnClassSuffixes, ' ')\n            .replace(allBtnShapes, ' ');\n        this.data.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.data.className)) {\n            this.isButton = true;\n        }\n\n        this.renderingPromise = new Promise(resolve => this._renderingResolver = resolve);\n    },\n    /**\n     * @override\n     */\n    start: async function () {\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.data.iniClassName);\n                }\n                active = subActive;\n            } else {\n                active = !this.data.iniClassName\n                         || this.toleratedClasses.some(val => this.data.iniClassName.split(' ').includes(val))\n                         || !this.data.iniClassName.includes('btn-');\n            }\n            this._setSelectOption($option, active);\n        }\n\n        const _super = this._super.bind(this);\n\n        this._updateOptionsUI();\n\n        if (this.data.url) {\n            var match = /mailto:(.+)/.exec(this.data.url);\n            this.$('input[name=\"url\"]').val(match ? match[1] : this.data.url);\n            this._onURLInput();\n            this._savedURLInputOnDestroy = false;\n        }\n\n        if (!this.noFocusUrl) {\n            this.focusUrl();\n        }\n\n        return _super(...arguments);\n    },\n    /**\n     * @private\n     */\n    async _widgetRenderAndInsert() {\n        const res = await this._super(...arguments);\n\n        // TODO find a better solution than this during the upcoming refactoring\n        // of the link tools / link dialog.\n        if (this._renderingResolver) {\n            this._renderingResolver();\n        }\n\n        return res;\n    },\n    /**\n     * @override\n     */\n    destroy () {\n        if (this._savedURLInputOnDestroy) {\n            this._adaptPreview();\n        }\n        this._super(...arguments);\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: function (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.data.iniClassName && this.data.iniClassName.split(' ').includes(linkClass)) {\n                    data.classes += \" btn \" + linkClass;\n                }\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) || isColorGradient(data.customFill) ? '' : data.customFill);\n            this.$link.css('background-image', 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.data.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.querySelector('input[name=\"url\"]');\n        urlInput.focus();\n        urlInput.select();\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     */\n    getOrCreateLink (options) {\n        Link.getOrCreateLink(options);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Abstract method: adapt the link to changes.\n     *\n     * @abstract\n     * @private\n     */\n    _adaptPreview: function () {},\n    /**\n     * @private\n     */\n    _correctLink: function (url) {\n        if (url.indexOf('mailto:') === 0 || url.indexOf('tel:') === 0) {\n            url = url.replace(/^tel:([0-9]+)$/, 'tel://$1');\n        } else if (url.indexOf('@') !== -1 && url.indexOf(':') === -1) {\n            url = 'mailto:' + url;\n        } else if (url && url.indexOf('://') === -1 && url[0] !== '/'\n                    && url[0] !== '#' && url.slice(0, 2) !== '${') {\n            url = 'http://' + url;\n        }\n        return url;\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: function () {},\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: function () {\n        var $url = this.$('input[name=\"url\"]');\n        var url = $url.val();\n        var content = this.$('input[name=\"label\"]').val() || url;\n\n        if (!this.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.data.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        if (\n            url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0 && !url.match(/^http[s]?/i) ||\n            this._link && this._link.href.includes('mailto:') && !url.includes('mailto:')\n        ) {\n            url = ('mailto:' + url);\n        } else if (url.indexOf(location.origin) === 0 && doStripDomain) {\n            url = url.slice(location.origin.length);\n        }\n        var allWhitespace = /\\s+/gi;\n        var allStartAndEndSpace = /^\\s+|\\s+$/gi;\n        return {\n            content: content,\n            url: this._correctLink(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.data.oldAttributes,\n            isNewWindow: isNewWindow,\n            doStripDomain: doStripDomain,\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: function (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: function () {},\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: function () {},\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: function () {},\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: function () {},\n    /**\n     * Returns the custom text color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomTextColor: function () {},\n    /**\n     * Returns the custom border color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorder: function () {},\n    /**\n     * Returns the custom border width for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorderWidth: function () {},\n    /**\n     * Returns the custom border style for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomBorderStyle: function () {},\n    /**\n     * Returns the custom fill color for custom type.\n     *\n     * @abstract\n     * @private\n     * @returns {string}\n     */\n    _getLinkCustomFill: function () {},\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: function () {},\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: function (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: function ($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._setLinkContent && (linkInfos.content !== this.data.originalText || linkInfos.url !== this.data.url))) {\n            if (linkInfos.content === this.data.originalText) {\n                $link.html(this.data.originalHTML);\n            } else if (linkInfos.content && linkInfos.content.length) {\n                $link.text(linkInfos.content);\n            } else {\n                $link.text(linkInfos.url);\n            }\n        }\n    },\n    /**\n     * @abstract\n     * @private\n     */\n    _updateOptionsUI: function () {},\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAnyChange: function (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: function () {\n        this._onURLInput(...arguments);\n    },\n    /**\n     * @private\n     */\n    _onURLInput: function () {\n        this._savedURLInputOnDestroy = true;\n        var $linkUrlInput = this.$('#o_link_dialog_url_input');\n        let value = $linkUrlInput.val();\n        let isLink = value.indexOf('@') < 0;\n        this._getIsNewWindowFormRow().toggleClass('d-none', !isLink);\n        this.$('.o_strip_domain').toggleClass('d-none', value.indexOf(window.location.origin) !== 0);\n    },\n    /**\n     * @private\n     */\n    _onURLInputChange: function () {\n        this._adaptPreview();\n        this._savedURLInputOnDestroy = false;\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 */\nLink.getOrCreateLink = ({ containerNode, startNode } = {})  => {\n\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    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\nreturn Link;\n});\n", "odoo.define('wysiwyg.widgets.LinkDialog', function (require) {\n'use strict';\n\nconst Dialog = require('wysiwyg.widgets.Dialog');\nconst Link = require('wysiwyg.widgets.Link');\n\n\n// This widget is there only to extend Link and be instantiated by LinkDialog.\nconst _DialogLinkWidget = Link.extend({\n    template: 'wysiwyg.widgets.link',\n    events: _.extend({}, Link.prototype.events || {}, {\n        'change [name=\"link_style_color\"]': '_onTypeChange',\n    }),\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.buttonOptsCollapseEl = this.el.querySelector('#o_link_dialog_button_opts_collapse');\n        this.$styleInputs = this.$('input.link-style');\n        this.$styleInputs.prop('checked', false).filter('[value=\"\"]').prop('checked', true);\n        if (this.data.isNewWindow) {\n            this.$('we-button.o_we_checkbox_wrapper').toggleClass('active', true);\n        }\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    save: function () {\n        var data = this._getData();\n        if (data === null) {\n            var $url = this.$('input[name=\"url\"]');\n            $url.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');\n            $url.focus();\n            return Promise.reject();\n        }\n        this.data.content = data.content;\n        this.data.url = data.url;\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        this.data.classes = data.classes.replace(allWhitespace, ' ').replace(allStartAndEndSpace, '');\n        if (data.classes.replace(allBtnTypes, ' ')) {\n            this.data.style = {\n                'background-color': '',\n                'color': '',\n            };\n        }\n        this.data.isNewWindow = data.isNewWindow;\n        this.final_data = this.data;\n        return Promise.resolve();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _adaptPreview: function () {\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.$(\"#link-preview\");\n        $linkPreview.attr(attrs);\n        this._updateLinkContent($linkPreview, data, { force: true });\n    },\n    /**\n     * @override\n     */\n    _doStripDomain: function () {\n        return this.$('#o_link_dialog_url_strip_domain').prop('checked');\n    },\n    /**\n     * @override\n     */\n    _getIsNewWindowFormRow() {\n        return this.$('input[name=\"is_new_window\"]').closest('.form-group');\n    },\n    /**\n     * @override\n     */\n    _getLinkOptions: function () {\n        const options = [\n            'input[name=\"link_style_color\"]',\n            'select[name=\"link_style_size\"] > option',\n            'select[name=\"link_style_shape\"] > option',\n        ];\n        return this.$(options.join(','));\n    },\n    /**\n     * @override\n     */\n    _getLinkShape: function () {\n        return this.$('select[name=\"link_style_shape\"]').val() || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkSize: function () {\n        return this.$('select[name=\"link_style_size\"]').val() || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkType: function () {\n        return this.$('input[name=\"link_style_color\"]:checked').val() || '';\n    },\n    /**\n     * @private\n     */\n    _isFromAnotherHostName: function (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 (ignored) {\n            return true;\n        }\n    },\n    /**\n     * @override\n     */\n    _isNewWindow: function (url) {\n        if (this.options.forceNewWindow) {\n            return this._isFromAnotherHostName(url);\n        } else {\n            return this.$('input[name=\"is_new_window\"]').prop('checked');\n        }\n    },\n    /**\n     * @override\n     */\n    _setSelectOption: function ($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: function () {\n        const el = this.el.querySelector('[name=\"link_style_color\"]: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: function () {\n        this._super(...arguments);\n        this.$('#o_link_dialog_url_input').closest('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');\n        this._adaptPreview();\n    },\n});\n\n/**\n * Allows to customize link content and style.\n */\nconst LinkDialog = Dialog.extend({\n    init: function (parent, ...args) {\n        this._super(...arguments);\n        this.linkWidget = new _DialogLinkWidget(this, ...args);\n    },\n    start: async function () {\n        const res = await this._super(...arguments);\n        await this.linkWidget.appendTo(this.$el);\n        return res;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    save: function () {\n        const _super = this._super.bind(this);\n        const saveArguments = arguments;\n        return this.linkWidget.save().then(() => {\n            this.final_data = this.linkWidget.final_data;\n            return _super(...saveArguments);\n        });\n    },\n});\n\nreturn LinkDialog;\n});\n", "/** @odoo-module **/\n\nimport Widget from 'web.Widget';\nimport {_t} from 'web.core';\nimport {DropPrevious} from 'web.concurrency';\nimport { ancestors } from '@web_editor/js/common/wysiwyg_utils';\n\nconst LinkPopoverWidget = Widget.extend({\n    template: 'wysiwyg.widgets.link.edit.tooltip',\n    xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],\n    events: {\n        'click .o_we_remove_link': '_onRemoveLinkClick',\n        'click .o_we_edit_link': '_onEditLinkClick',\n    },\n\n    /**\n     * @constructor\n     * @param {Element} target: target Element for which we display a popover\n     * @param {Wysiwyg} [option.wysiwyg]: The wysiwyg editor\n     */\n    init(parent, target, options) {\n        this._super(...arguments);\n        this.options = options;\n        this.target = target;\n        this.$target = $(target);\n        this.href = this.$target.attr('href'); // for template\n        this._dp = new DropPrevious();\n    },\n    /**\n     *\n     * @override\n     */\n    start() {\n        this.$urlLink = this.$('.o_we_url_link');\n        this.$previewFaviconImg = this.$('.o_we_preview_favicon img');\n        this.$previewFaviconFa = this.$('.o_we_preview_favicon .fa');\n        this.$copyLink = this.$('.o_we_copy_link');\n        this.$fullUrl = this.$('.o_we_full_url');\n\n        // Use the right ClipboardJS with respect to the prototype of this.el\n        // since, starting with Firefox 109, a widget element prototype that is\n        // adopted by an iframe will not be instanceof its original constructor.\n        // See: https://github.com/webcompat/web-bugs/issues/118350\n        const ClipboardJS =\n            this.el instanceof HTMLElement\n                ? window.ClipboardJS\n                : this.el.ownerDocument.defaultView.ClipboardJS;\n        // Copy onclick handler\n        const clipboard = new ClipboardJS(\n            this.$copyLink[0],\n            {text: () => this.target.href} // Absolute href\n        );\n        clipboard.on('success', () => {\n            this.$copyLink.tooltip('hide');\n            this.displayNotification({\n                type: 'success',\n                message: _t(\"Link copied to clipboard.\"),\n            });\n            this.popover.hide();\n        });\n\n        // init tooltips & popovers\n        this.$('[data-toggle=\"tooltip\"]').tooltip({\n            delay: 0,\n            placement: 'bottom',\n            container: this.options.wysiwyg.odooEditor.document.body,\n        });\n        const tooltips = [];\n        for (const el of this.$('[data-toggle=\"tooltip\"]').toArray()) {\n            tooltips.push($(el).data('bs.tooltip'));\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.options.wysiwyg.odooEditor.document.body,\n        })\n        .on('show.bs.popover.link_popover', () => {\n            this.options.wysiwyg.odooEditor.observerUnactive('show.bs.popover');\n            this._loadAsyncLinkPreview();\n            popoverShown = true;\n        })\n        .on('inserted.bs.popover', () => {\n            this.options.wysiwyg.odooEditor.observerActive('show.bs.popover');\n        })\n        .on('hide.bs.popover.link_popover', () => {\n            this.options.wysiwyg.odooEditor.observerUnactive('hide.bs.popover');\n            popoverShown = false;\n        })\n        .on('hidden.bs.popover.link_popover', () => {\n            this.options.wysiwyg.odooEditor.observerActive('hide.bs.popover');\n            for (const tooltip of tooltips) {\n                tooltip.hide();\n            }\n        })\n        .on('inserted.bs.popover.link_popover', () => {\n            this.$target.data('bs.popover').tip.classList.add('o_edit_menu_popover');\n        })\n        .popover('show');\n\n\n        this.popover = this.$target.data('bs.popover');\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.options.wysiwyg.odooEditor.document) {\n            $(this.options.wysiwyg.odooEditor.document).on('mouseup.link_popover', onClickDocument);\n        }\n\n        return this._super(...arguments);\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.options.wysiwyg.odooEditor.document).off('.link_popover');\n        this.$target.popover('dispose');\n        return this._super(...arguments);\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 (e) {\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.displayNotification({\n                type: 'danger',\n                message: _t(\"This URL is invalid. Preview couldn't be updated.\"),\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._dp.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                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.options.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.options.wysiwyg.removeLink();\n        ev.stopImmediatePropagation();\n        this.popover.hide();\n    },\n});\n\nLinkPopoverWidget.createFor = async function (parent, targetEl, options) {\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 = $(targetEl).data('bs.popover');\n    if (alreadyPopover || $(targetEl).is(noLinkPopoverClass) || !!$(targetEl).parents(noLinkPopoverClass).length) {\n        return null;\n    }\n    const popoverWidget = new this(parent, targetEl, options);\n    const wysiwyg = $('#wrapwrap').data('wysiwyg');\n    if (wysiwyg) {\n        wysiwyg.odooEditor.observerUnactive('LinkPopoverWidget');\n    }\n    await popoverWidget.appendTo(targetEl)\n    if (wysiwyg) {\n        wysiwyg.odooEditor.observerActive('LinkPopoverWidget');\n    }\n    return popoverWidget;\n};\n\nexport default LinkPopoverWidget;\n", "odoo.define('wysiwyg.widgets.LinkTools', function (require) {\n'use strict';\n\nconst Link = require('wysiwyg.widgets.Link');\nconst {ColorPaletteWidget} = require('web_editor.ColorPalette');\nconst {ColorpickerWidget} = require('web.Colorpicker');\nconst {\n    computeColorClasses,\n    getCSSVariableValue,\n    getColorClass,\n    getNumericAndUnit,\n    isColorGradient,\n} = require('web_editor.utils');\n\n/**\n * Allows to customize link content and style.\n */\nconst LinkTools = Link.extend({\n    template: 'wysiwyg.widgets.linkTools',\n    events: _.extend({}, Link.prototype.events, {\n        'click we-select we-button': '_onPickSelectOption',\n        'click we-checkbox': '_onClickCheckbox',\n        'change .link-custom-color-border input': '_onChangeCustomBorderWidth',\n        'keypress .link-custom-color-border input': '_onKeyPressCustomBorderWidth',\n        'click we-select [name=\"link_border_style\"] we-button': '_onBorderStyleSelectOption',\n    }),\n\n    /**\n     * @override\n     */\n    init: function (parent, options, editable, data, $button, link) {\n        this._link = link;\n        this._observer = new MutationObserver(() =>{\n            this._setLinkContent = false;\n            this._observer.disconnect();\n        });\n        this._observer.observe(this._link, {subtree: true, childList: true, characterData: true});\n        this._super(parent, options, editable, data, $button, this._link);\n        // Keep track of each selected custom color and colorpicker.\n        this.customColors = {};\n        this.colorpickers = {};\n        // TODO remove me in master: we still save the promises indicating when\n        // each colorpicker is fully instantiated but we now make sure to never\n        // use them while they are not, without this.\n        this.colorpickersPromises = {};\n        this.COLORPICKER_CSS_PROPERTIES = ['color', 'background-color', 'border-color'];\n        this.PREFIXES = {\n            'color': 'text-',\n            'background-color': 'bg-',\n        };\n    },\n    /**\n     * @override\n     */\n    willStart: async function () {\n        const _super = this._super.bind(this);\n        await Promise.all(this.COLORPICKER_CSS_PROPERTIES.map(cssProperty => this._addColorPicker(cssProperty)));\n        return _super(...arguments);\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        const titleEls = this.el.querySelectorAll('we-title');\n        for (const titleEl of titleEls) {\n            // See _buildTitleElement for explanation\n            titleEl.textContent = titleEl.textContent.replace(/\u2319/g, '\u2514');\n        }\n        this._addHintClasses();\n        const ret = await this._super(...arguments);\n        const link = this.$link[0];\n        const colorpickerLocations = {\n            'color': '.link-custom-color-text .dropdown-menu',\n            'background-color': '.link-custom-color-fill .dropdown-menu',\n            'border-color': '.link-custom-color-border .o_we_so_color_palette .dropdown-menu',\n        };\n        for (const cssProperty of this.COLORPICKER_CSS_PROPERTIES) {\n            // Colorpickers were created into fragments before any UI or event\n            // handler of this main widget was built. This just moves those\n            // colorpickers at their rightful position, synchronously.\n            const locationEl = this.el.querySelector(colorpickerLocations[cssProperty]);\n            this.colorpickers[cssProperty].$el.appendTo(locationEl);\n        }\n        const customStyleProps = ['color', 'background-color', 'background-image', 'border-width', 'border-style', 'border-color'];\n        if (customStyleProps.some(s => link.style[s])) {\n            // Force custom style if style exists on the link.\n            const customOption = this.el.querySelector('[name=\"link_style_color\"] we-button[data-value=\"custom\"]');\n            this._setSelectOption($(customOption), true);\n            this._updateOptionsUI();\n        }\n        return ret;\n    },\n    destroy: function () {\n        if (!this.el) {\n            return this._super(...arguments);\n        }\n        const $contents = this.$link.contents();\n        if (this.shouldUnlink()) {\n            $contents.unwrap();\n        }\n        this._observer.disconnect();\n        this._super(...arguments);\n        this._removeHintClasses();\n    },\n    shouldUnlink: function () {\n        return !this.$link.attr('href') && !this.colorCombinationClass\n    },\n    applyLinkToDom() {\n        this._observer.disconnect();\n        this._removeHintClasses();\n        this._super(...arguments);\n        this.options.wysiwyg.odooEditor.historyStep();\n        this._addHintClasses();\n        this._observer.observe(this._link, {subtree: true, childList: true, characterData: true});\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    focusUrl() {\n        this.el.scrollIntoView();\n        this._super();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _adaptPreview: function () {\n        var data = this._getData();\n        if (data === null) {\n            return;\n        }\n        this.applyLinkToDom(data);\n    },\n    /**\n     * @override\n     */\n    _doStripDomain: function () {\n        return this.$('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.$('we-checkbox[name=\"is_new_window\"]').closest('we-row');\n    },\n    /**\n     * @override\n     */\n    _getLinkOptions: function () {\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.$(options.join(','));\n    },\n    /**\n     * @override\n     */\n    _getLinkShape: function () {\n        return this.$('we-selection-items[name=\"link_style_shape\"] we-button.active').data('value') || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkSize: function () {\n        return this.$('we-selection-items[name=\"link_style_size\"] we-button.active').data('value') || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkType: function () {\n        return this.$('we-selection-items[name=\"link_style_color\"] we-button.active').data('value') || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomTextColor: function () {\n        return this.customColors['color'];\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomBorder: function () {\n        return this.customColors['border-color'];\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomBorderWidth: function () {\n        return this.$('.link-custom-color-border input').val() || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomBorderStyle: function () {\n        return this.$('.link-custom-color-border we-button.active').data('value') || '';\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomFill: function () {\n        return this.customColors['background-color'];\n    },\n    /**\n     * @override\n     */\n    _getLinkCustomClasses: function () {\n        let textClass = this.customColors['color'];\n        const colorPickerFg = this.colorpickers['color'];\n        if (\n            !textClass ||\n            !colorPickerFg ||\n            !computeColorClasses(colorPickerFg.getColorNames(), 'text-').includes(textClass)\n        ) {\n            textClass = '';\n        }\n        let fillClass = this.customColors['background-color'];\n        const colorPickerBg = this.colorpickers['background-color'];\n        if (\n            !fillClass ||\n            !colorPickerBg ||\n            !computeColorClasses(colorPickerBg.getColorNames(), 'bg-').includes(fillClass)\n        ) {\n            fillClass = '';\n        }\n        return ` ${textClass} ${fillClass}`;\n    },\n    /**\n     * @override\n     */\n    _isNewWindow: function () {\n        return this.$('we-checkbox[name=\"is_new_window\"]').closest('we-button.o_we_checkbox_wrapper').hasClass('active');\n    },\n    /**\n     * @override\n     */\n    _setSelectOption: function ($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: function () {\n        const el = this.el.querySelector('[name=\"link_style_color\"] we-button.active');\n        if (el) {\n            this.colorCombinationClass = el.dataset.value;\n            // Hide the size and shape options if the link is an unstyled anchor.\n            this.$('.link-size-row, .link-shape-row').toggleClass('d-none', !this.colorCombinationClass);\n            // Show custom colors only for Custom style.\n            this.$('.link-custom-color').toggleClass('d-none', el.dataset.value !== 'custom');\n\n            // Note: the _updateColorpicker method is supposedly async but can\n            // be used synchronously given the fact that _addColorPicker was\n            // called during this widget initialization.\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 = getNumericAndUnit(borderWidth);\n            this.$('.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.$(`.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: async function (cssProperty) {\n        const prefix = this.PREFIXES[cssProperty];\n        let colorpicker = this.colorpickers[cssProperty];\n\n        if (!colorpicker) {\n            // As a fix, we made it possible to use this method always\n            // synchronously. This is just kept as compatibility.\n            // TODO Remove in master.\n            colorpicker = await this._addColorPicker(cssProperty);\n        }\n\n        // Update selected color.\n        const colorNames = colorpicker.getColorNames();\n        let color = this.linkEl.style[cssProperty];\n        const colorClasses = prefix ? computeColorClasses(colorNames, prefix) : [];\n        const colorClass = prefix && 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 (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 = colorpicker.colorToColorNames[ColorpickerWidget.normalizeCSSColor(color)];\n            colorpicker.setSelectedColor(null, colorName || color, false);\n        } else {\n            colorpicker.setSelectedColor(null, isColorClass ? color.replace(prefix, '') : color, false);\n        }\n\n        // Update preview.\n        const $colorPreview = this.$('.link-custom-color-' + (cssProperty === 'border-color' ? 'border' : cssProperty === 'color' ? 'text' : 'fill') + ' .o_we_color_preview');\n        const previewClasses = computeColorClasses(colorNames, 'bg-');\n        $colorPreview[0].classList.remove(...previewClasses);\n        if (isColorClass) {\n            $colorPreview.addClass(`bg-${color.replace(prefix, '')}`);\n            $colorPreview.css('background-color', '');\n            $colorPreview.css('background-image', '');\n        } else {\n            $colorPreview.css('background-color', isColorGradient(color) ? 'rgba(0, 0, 0, 0)' : color);\n            $colorPreview.css('background-image', isColorGradient(color) ? color : '');\n        }\n    },\n    /**\n     * @private\n     * @param {string} cssProperty\n     */\n    async _addColorPicker(cssProperty) {\n        const prefix = this.PREFIXES[cssProperty];\n        const colorpicker = new ColorPaletteWidget(this, {\n            excluded: ['transparent_grayscale'],\n            $editable: $(this.options.wysiwyg.odooEditor.editable),\n            withGradients: cssProperty === 'background-color',\n        });\n        this.colorpickers[cssProperty] = colorpicker;\n        this.colorpickersPromises[cssProperty] = colorpicker.appendTo(document.createDocumentFragment());\n        await this.colorpickersPromises[cssProperty];\n        colorpicker.on('custom_color_picked color_picked color_hover color_leave', this, (ev) => {\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            if (['custom_color_picked', 'color_picked'].includes(ev.name)) {\n                const selection = window.getSelection();\n                const range = document.createRange();\n                range.selectNodeContents(this.linkEl);\n                selection.removeAllRanges();\n                selection.addRange(range);\n                this.options.wysiwyg.odooEditor.execCommand('applyColor', '', 'color');\n                this.options.wysiwyg.odooEditor.execCommand('applyColor', '', 'backgroundColor');\n            }\n\n            let color = ev.data.color;\n            const colorNames = colorpicker.getColorNames();\n            const colorClasses = prefix ? 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 = getCSSVariableValue(color);\n            }\n            this.customColors[cssProperty] = color;\n            this.applyLinkToDom(this._getData());\n            if (['custom_color_picked', 'color_picked'].includes(ev.name)) {\n                this.options.wysiwyg.odooEditor.historyStep();\n                this._updateOptionsUI();\n            }\n        });\n        return colorpicker;\n    },\n    /**\n     * Add hint to the classes of the link and button.\n     */\n    _addHintClasses () {\n        this.$link.addClass('oe_edited_link');\n        this.$button.addClass('active');\n    },\n    /**\n     * Remove hint to the classes of the link and button.\n     */\n    _removeHintClasses () {\n        $(this.options.wysiwyg.odooEditor.document).find('.oe_edited_link').removeClass('oe_edited_link');\n        this.$button.removeClass('active');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    _onClickCheckbox: function (ev) {\n        const $target = $(ev.target);\n        $target.closest('we-button.o_we_checkbox_wrapper').toggleClass('active');\n        this._adaptPreview();\n    },\n    _onPickSelectOption: function (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: function (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: function (ev) {\n        if (ev.keyCode === $.ui.keyCode.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: function (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.options.wysiwyg.odooEditor.historyStep();\n        }\n    },\n    /**\n     * @override\n     */\n    __onURLInput() {\n        this._super(...arguments);\n        this.options.wysiwyg.odooEditor.historyPauseSteps('_onURLInput');\n        this._adaptPreview();\n        this.options.wysiwyg.odooEditor.historyUnpauseSteps('_onURLInput');\n    },\n});\n\nreturn LinkTools;\n});\n", "odoo.define('wysiwyg.widgets.media', function (require) {\n'use strict';\n\nvar concurrency = require('web.concurrency');\nconst config = require('web.config');\nvar core = require('web.core');\nvar Dialog = require('web.Dialog');\nvar dom = require('web.dom');\nvar fonts = require('wysiwyg.fonts');\nvar utils = require('web.utils');\nvar Widget = require('web.Widget');\nvar session = require('web.session');\nconst {removeOnImageChangeAttrs} = require('web_editor.image_processing');\nconst {getCSSVariableValue, DEFAULT_PALETTE} = require('web_editor.utils');\nconst { UploadProgressToast } = require('@web_editor/js/wysiwyg/widgets/upload_progress_toast');\n\nvar QWeb = core.qweb;\nvar _t = core._t;\n\nvar MediaWidget = Widget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],\n\n    /**\n     * @constructor\n     * @param {Element} media: the target Element for which we select a media\n     * @param {Object} options: useful parameters such as res_id, res_model,\n     *  context, user_id, ...\n     */\n    init: function (parent, media, options) {\n        this._super.apply(this, arguments);\n        this.media = media;\n        this.$media = $(media);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @todo comment\n     */\n    clear: function () {\n        if (!this.media) {\n            return;\n        }\n        this._clear();\n    },\n    /**\n     * Saves the currently configured media on the target media.\n     *\n     * @abstract\n     * @returns {Promise}\n     */\n    save: function () {},\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @abstract\n     */\n    _clear: function () {},\n});\n\nvar SearchableMediaWidget = MediaWidget.extend({\n    events: _.extend({}, MediaWidget.prototype.events || {}, {\n        'keydown .o_we_search': '_onSearchKeydown',\n        'input .o_we_search': '_onSearchInput',\n    }),\n\n    /**\n     * @constructor\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n        this._onSearchInput = _.debounce(this._onSearchInput, 500);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Finds and displays existing attachments related to the target media.\n     *\n     * @abstract\n     * @param {string} needle: only return attachments matching this parameter\n     * @returns {Promise}\n     */\n    search: function (needle) {},\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Renders thumbnails for the attachments.\n     *\n     * @abstract\n     * @returns {Promise}\n     */\n    _renderThumbnails: function () {},\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onSearchKeydown: function (ev) {\n        // If the template contains a form that has only one input, the enter\n        // will reload the page as the html 2.0 specify this behavior.\n        if (ev.originalEvent && (ev.originalEvent.code === \"Enter\" || ev.originalEvent.key === \"Enter\")) {\n            ev.preventDefault();\n        }\n    },\n    /**\n     * @private\n     */\n    _onSearchInput: function (ev) {\n        this.attachments = [];\n        // Disable user interactions with attachments while updating results.\n        this.$('.o_we_existing_attachments').css('pointer-events', 'none');\n        this.search($(ev.currentTarget).val() || \"\")\n            .then(() => this._renderThumbnails())\n            .then(() => {\n                // Re-enable user interactions after updating results.\n                this.$(\".o_we_existing_attachments\").css(\"pointer-events\", \"\");\n            });\n        this.hasSearched = true;\n    },\n});\n\n/**\n * Let users choose a file, including uploading a new file in odoo.\n */\nvar FileWidget = SearchableMediaWidget.extend({\n    events: _.extend({}, SearchableMediaWidget.prototype.events || {}, {\n        'click .o_upload_media_button': '_onUploadButtonClick',\n        'change .o_file_input': '_onFileInputChange',\n        'click .o_upload_media_url_button': '_onUploadURLButtonClick',\n        'input .o_we_url_input': '_onURLInputChange',\n        'click .o_existing_attachment_cell': '_onAttachmentClick',\n        'click .o_existing_attachment_remove': '_onRemoveClick',\n        'click .o_load_more': '_onLoadMoreClick',\n    }),\n    existingAttachmentsTemplate: undefined,\n\n    IMAGE_MIMETYPES: ['image/jpg', 'image/jpeg', 'image/jpe', 'image/png', 'image/svg+xml', 'image/gif'],\n    IMAGE_EXTENSIONS: ['.jpg', '.jpeg', '.jpe', '.png', '.svg', '.gif'],\n    NUMBER_OF_ATTACHMENTS_TO_DISPLAY: 30,\n    MAX_DB_ATTACHMENTS: 5,\n\n    /**\n     * @constructor\n     */\n    init: function (parent, media, options) {\n        this._super.apply(this, arguments);\n        this._mutex = new concurrency.Mutex();\n\n        this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;\n\n        this.options = _.extend({\n            mediaWidth: media && media.parentElement && $(media.parentElement).width(),\n            useMediaLibrary: true,\n        }, options || {});\n\n        this.attachments = [];\n        this.selectedAttachments = [];\n        this.libraryMedia = [];\n        this.selectedMedia = [];\n\n        this._onUploadURLButtonClick = dom.makeAsyncHandler(this._onUploadURLButtonClick);\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        var def = this._super.apply(this, arguments);\n        var self = this;\n        this.$urlInput = this.$('.o_we_url_input');\n        this.$form = this.$('form');\n        this.$fileInput = this.$('.o_file_input');\n        this.$uploadButton = this.$('.o_upload_media_button');\n        this.$addUrlButton = this.$('.o_upload_media_url_button');\n        this.$urlSuccess = this.$('.o_we_url_success');\n        this.$urlWarning = this.$('.o_we_url_warning');\n        this.$urlError = this.$('.o_we_url_error');\n        this.$errorText = this.$('.o_we_error_text');\n\n        // If there is already an attachment on the target, select by default\n        // that attachment if it is among the loaded images.\n        var o = {\n            url: null,\n            alt: null,\n        };\n        if (this.$media.is('img')) {\n            o.url = this.$media.attr('src');\n        } else if (this.$media.is('a.o_image')) {\n            o.url = this.$media.attr('href').replace(/[?].*/, '');\n            o.id = +o.url.match(/\\/web\\/content\\/(\\d+)/, '')[1];\n        }\n\n        return this.search('').then(async () => {\n            await this._renderThumbnails();\n            if (o.url) {\n                self._selectAttachement(_.find(self.attachments, function (attachment) {\n                    return o.url === attachment.image_src;\n                }) || o);\n            }\n            return def;\n        });\n    },\n    /**\n     * @override\n     */\n    destroy() {\n        if (this.uploader) {\n            // Prevent uploader from being destroyed with call to super so it can linger\n            this.uploader.setParent(null);\n            this.uploader.close(2000);\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Saves the currently selected image on the target media. If new files are\n     * currently being added, delays the save until all files have been added.\n     *\n     * @override\n     */\n    save: function () {\n        return this._mutex.exec(this._save.bind(this));\n    },\n    /**\n     * @override\n     */\n    search: function (needle) {\n        this.needle = needle;\n        return this.fetchAttachments(this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY, 0);\n    },\n    /**\n     * @param {Number} number - the number of attachments to fetch\n     * @param {Number} offset - from which result to start fetching\n     */\n    fetchAttachments: function (number, offset) {\n        return this._rpc({\n            model: 'ir.attachment',\n            method: 'search_read',\n            args: [],\n            kwargs: {\n                domain: this._getAttachmentsDomain(this.needle),\n                fields: ['name', 'mimetype', 'description', 'checksum', 'url', 'type', 'res_id', 'res_model', 'public', 'access_token', 'image_src', 'image_width', 'image_height', 'original_id'],\n                order: [{name: 'id', asc: false}],\n                context: this.options.context,\n                // Try to fetch first record of next page just to know whether there is a next page.\n                limit: number + 1,\n                offset: offset,\n            },\n        }).then(attachments => {\n            this.attachments = this.attachments.slice();\n            Array.prototype.splice.apply(this.attachments, [offset, attachments.length].concat(attachments));\n        });\n    },\n    /**\n     * Computes whether there is content to display in the template.\n     */\n    hasContent() {\n        return this.attachments.length;\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _clear: function () {\n        this.media.className = this.media.className && this.media.className.replace(/(^|\\s+)(o_image)(?=\\s|$)/g, ' ');\n    },\n    /**\n     * Returns the domain for attachments used in media dialog.\n     * We look for attachments related to the current document. If there is a value for the model\n     * field, it is used to search attachments, and the attachments from the current document are\n     * filtered to display only user-created documents.\n     * In the case of a wizard such as mail, we have the documents uploaded and those of the model\n     *\n     * @private\n     * @params {string} needle\n     * @returns {Array} \"ir.attachment\" odoo domain.\n     */\n    _getAttachmentsDomain: function (needle) {\n        var domain = this.options.attachmentIDs && this.options.attachmentIDs.length ? ['|', ['id', 'in', this.options.attachmentIDs]] : [];\n\n        var attachedDocumentDomain = [\n            '&',\n            ['res_model', '=', this.options.res_model],\n            ['res_id', '=', this.options.res_id|0]\n        ];\n        // if the document is not yet created, do not see the documents of other users\n        if (!this.options.res_id) {\n            attachedDocumentDomain.unshift('&');\n            attachedDocumentDomain.push(['create_uid', '=', this.options.user_id]);\n        }\n        if (this.options.data_res_model) {\n            var relatedDomain = ['&',\n                ['res_model', '=', this.options.data_res_model],\n                ['res_id', '=', this.options.data_res_id|0]];\n            if (!this.options.data_res_id) {\n                relatedDomain.unshift('&');\n                relatedDomain.push(['create_uid', '=', session.uid]);\n            }\n            domain = domain.concat(['|'], attachedDocumentDomain, relatedDomain);\n        } else {\n            domain = domain.concat(attachedDocumentDomain);\n        }\n        domain = ['|', ['public', '=', true]].concat(domain);\n        domain = domain.concat(this.options.mimetypeDomain);\n        if (needle && needle.length) {\n            domain.push(['name', 'ilike', needle]);\n        }\n        if (!this.options.useMediaLibrary) {\n            domain.push('|', ['url', '=', false], '!', ['url', '=ilike', '/web_editor/shape/%']);\n        }\n        domain.push('!', ['name', '=like', '%.crop']);\n        domain.push('|', ['type', '=', 'binary'], '!', ['url', '=like', '/%/static/%']);\n        return domain;\n    },\n    /**\n     * @private\n     */\n    _highlightSelected: function () {\n        var self = this;\n        this.$('.o_existing_attachment_cell.o_we_attachment_selected').removeClass(\"o_we_attachment_selected\");\n        _.each(this.selectedAttachments, function (attachment) {\n            self.$('.o_existing_attachment_cell[data-id=' + attachment.id + ']')\n                .addClass(\"o_we_attachment_selected\").css('display', '');\n        });\n    },\n    /**\n     * @private\n     * @param {object} attachment\n     */\n    _handleNewAttachment: function (attachment) {\n        this.attachments = this.attachments.filter(att => att.id !== attachment.id);\n        this.attachments.unshift(attachment);\n        this._renderThumbnails();\n        this._selectAttachement(attachment);\n    },\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _loadMoreImages: function (forceSearch) {\n        return this.fetchAttachments(10, this.numberOfAttachmentsToDisplay).then(() => {\n            this.numberOfAttachmentsToDisplay += 10;\n            if (!forceSearch) {\n                this._renderThumbnails();\n                return Promise.resolve();\n            } else {\n                return this.search(this.$('.o_we_search').val() || '');\n            }\n        });\n    },\n    /**\n     * Renders the existing attachments and returns the result as a string.\n     *\n     * @param {Object[]} attachments\n     * @returns {string}\n     */\n    _renderExisting: function (attachments) {\n        return QWeb.render(this.existingAttachmentsTemplate, {\n            attachments: attachments,\n            widget: this,\n        });\n    },\n    /**\n     * @private\n     */\n    _renderThumbnails: function () {\n        var attachments = this.attachments.slice(0, this.numberOfAttachmentsToDisplay);\n\n        // Render menu & content\n        this.$('.o_we_existing_attachments').replaceWith(\n            this._renderExisting(attachments)\n        );\n\n        this._highlightSelected();\n\n        // adapt load more\n        this.$('.o_we_load_more').toggleClass('d-none', !this.hasContent());\n        var noLoadMoreButton = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY >= this.attachments.length;\n        var noMoreImgToLoad = this.numberOfAttachmentsToDisplay >= this.attachments.length;\n        this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad);\n        this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);\n    },\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _save: async function () {\n        // Create all media-library attachments.\n        const toSave = Object.fromEntries(this.selectedMedia.map(media => [\n            media.id, {\n                query: media.query || '',\n                is_dynamic_svg: !!media.isDynamicSVG,\n                dynamic_colors: media.dynamicColors,\n            }\n        ]));\n        let mediaAttachments = [];\n        if (Object.keys(toSave).length !== 0) {\n            mediaAttachments = await this._rpc({\n                route: '/web_editor/save_library_media',\n                params: {\n                    media: toSave,\n                },\n            });\n        }\n        const selected = this.selectedAttachments.concat(mediaAttachments).map(attachment => {\n            // Color-customize dynamic SVGs with the theme colors\n            if (attachment.image_src && attachment.image_src.startsWith('/web_editor/shape/')) {\n                const colorCustomizedURL = new URL(attachment.image_src, window.location.origin);\n                colorCustomizedURL.searchParams.forEach((value, key) => {\n                    const match = key.match(/^c([1-5])$/);\n                    if (match) {\n                        colorCustomizedURL.searchParams.set(key, getCSSVariableValue(`o-color-${match[1]}`))\n                    }\n                })\n                attachment.image_src = colorCustomizedURL.pathname + colorCustomizedURL.search;\n            }\n            return attachment;\n        });\n        if (this.options.multiImages) {\n            return selected;\n        }\n\n        const img = selected[0];\n        if (!img || !img.id || this.$media.attr('src') === img.image_src) {\n            return this.media;\n        }\n\n        if (!img.public && !img.access_token) {\n            await this._rpc({\n                model: 'ir.attachment',\n                method: 'generate_access_token',\n                args: [[img.id]]\n            }).then(function (access_token) {\n                img.access_token = access_token[0];\n            });\n        }\n\n        if (img.image_src) {\n            var src = img.image_src;\n            if (!img.public && img.access_token) {\n                src += _.str.sprintf('?access_token=%s', encodeURIComponent(img.access_token));\n            }\n            if (!this.$media.is('img')) {\n\n                // Note: by default the images receive the bootstrap opt-in\n                // img-fluid class. We cannot make them all responsive\n                // by design because of libraries and client databases img.\n                this.$media = $('<img/>', {class: 'img-fluid o_we_custom_image'});\n                this.media = this.$media[0];\n            }\n            this.$media.attr('src', src);\n        } else {\n            if (!this.$media.is('a')) {\n                $('.note-control-selection').hide();\n                this.$media = $('<a/>');\n                this.media = this.$media[0];\n            }\n            var href = '/web/content/' + img.id + '?';\n            if (!img.public && img.access_token) {\n                href += _.str.sprintf('access_token=%s&', img.access_token);\n            }\n            href += 'unique=' + img.checksum + '&download=true';\n            this.$media.attr('href', href);\n            this.$media.addClass('o_image').attr('title', img.name);\n        }\n\n        this.$media.attr('alt', img.alt || img.description || '');\n        var style = this.style;\n        if (style) {\n            this.$media.css(style);\n        }\n\n        // Remove image modification attributes\n        removeOnImageChangeAttrs.forEach(attr => {\n            delete this.media.dataset[attr];\n        });\n        // Add mimetype for documents\n        if (!img.image_src) {\n            this.media.dataset.mimetype = img.mimetype;\n        }\n        this.media.classList.remove('o_modified_image_to_save');\n        this.$media.trigger('image_changed');\n        return this.media;\n    },\n    /**\n     * @param {object} attachment\n     * @param {boolean} [save=true] to save the given attachment in the DOM and\n     *  and to close the media dialog\n     * @private\n     */\n    _selectAttachement: function (attachment, save, {type = 'attachment'} = {}) {\n        const possibleProps = {\n            'attachment': 'selectedAttachments',\n            'media': 'selectedMedia'\n        };\n        const prop = possibleProps[type];\n        if (this.options.multiImages) {\n            // if the clicked attachment is already selected then unselect it\n            // unless it was a save request (then keep the current selection)\n            const index = this[prop].indexOf(attachment);\n            if (index !== -1) {\n                if (!save) {\n                    this[prop].splice(index, 1);\n                }\n            } else {\n                // if the clicked attachment is not selected, add it to selected\n                this[prop].push(attachment);\n            }\n        } else {\n            Object.values(possibleProps).forEach(prop => {\n                this[prop] = [];\n            });\n            // select the clicked attachment\n            this[prop] = [attachment];\n        }\n        this._highlightSelected();\n        if (save) {\n            this.trigger_up('save_request');\n        }\n    },\n    /**\n     * Updates the add by URL UI.\n     *\n     * @private\n     * @param {boolean} emptyValue\n     * @param {boolean} isURL\n     * @param {boolean} isImage\n     */\n    _updateAddUrlUi: function (emptyValue, isURL, isImage) {\n        this.$addUrlButton.toggleClass('btn-secondary', emptyValue)\n            .toggleClass('btn-primary', !emptyValue)\n            .prop('disabled', !isURL);\n        this.$urlSuccess.toggleClass('d-none', !isURL);\n        this.$urlError.toggleClass('d-none', emptyValue || isURL);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onAttachmentClick: function (ev) {\n        const attachment = ev.currentTarget;\n        const {id: attachmentID, mediaId} = attachment.dataset;\n        if (attachmentID) {\n            const attachment = this.attachments.find(attachment => attachment.id === parseInt(attachmentID));\n            this._selectAttachement(attachment, !this.options.multiImages);\n        } else if (mediaId) {\n            const media = this.libraryMedia.find(media => media.id === parseInt(mediaId));\n            this._selectAttachement(media, !this.options.multiImages, {type: 'media'});\n        }\n    },\n    /**\n     * Handles change of the file input: create attachments with the new files\n     * and open the Preview dialog for each of them. Locks the save button until\n     * all new files have been processed.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    _onFileInputChange: function () {\n        return this._mutex.exec(this._addData.bind(this));\n    },\n    /**\n     * Uploads the files that are currently selected on the file input, which\n     * creates new attachments. Then inserts them on the media dialog and\n     * selects them. If multiImages is not set, also triggers up the\n     * save_request event to insert the attachment in the DOM.\n     *\n     * @private\n     * @returns {Promise}\n     */\n    async _addData() {\n        let files = this.$fileInput[0].files;\n        if (!files.length) {\n            // Case if the input is emptied, return resolved promise\n            return;\n        }\n\n        const uploadMutex = new concurrency.Mutex();\n\n        // Upload the smallest file first to block the user the least possible.\n        files = _.sortBy(files, 'size');\n        await this._setUpProgressToast(files);\n        _.each(files, (file, index) => {\n            // Upload one file at a time: no need to parallel as upload is\n            // limited by bandwidth.\n            uploadMutex.exec(async () => {\n                const dataURL = await utils.getDataURLFromFile(file);\n                const attachment = await this.uploader.rpcShowProgress({\n                    route: '/web_editor/attachment/add_data',\n                    params: {\n                        'name': file.name,\n                        'data': dataURL.split(',')[1],\n                        'res_id': this.options.res_id,\n                        'res_model': this.options.res_model,\n                        'is_image': this.widgetType === 'image',\n                        'width': 0,\n                        'quality': 0,\n                        'generate_access_token': true,\n                    }\n                }, index);\n                if (!attachment.error) {\n                    this.trigger_up('wysiwyg_attachment', attachment);\n                    this._handleNewAttachment(attachment);\n                }\n            });\n        });\n\n        return uploadMutex.getUnlockedDef().then(() => {\n            if (!this.uploader.hasError) {\n                this.uploader.close(3000);\n            }\n            if (!this.options.multiImages && !this.noSave) {\n                this.trigger_up('save_request');\n            }\n            this.noSave = false;\n        });\n    },\n    /**\n     * @private\n     */\n    _onRemoveClick: function (ev) {\n        var self = this;\n        ev.stopPropagation();\n        Dialog.confirm(this, _t(\"Are you sure you want to delete this file ?\"), {\n            confirm_callback: function () {\n                var $a = $(ev.currentTarget).closest('.o_existing_attachment_cell');\n                var id = parseInt($a.data('id'), 10);\n                var attachment = _.findWhere(self.attachments, {id: id});\n                 return self._rpc({\n                    route: '/web_editor/attachment/remove',\n                    params: {\n                        ids: [id],\n                    },\n                }).then(function (prevented) {\n                    if (_.isEmpty(prevented)) {\n                        self.attachments = _.without(self.attachments, attachment);\n                        self.attachments.filter(at => at.original_id[0] === attachment.id).forEach(at => delete at.original_id);\n                        if (!self.attachments.length) {\n                            self._renderThumbnails(); //render the message and image if empty\n                        } else {\n                            $a.closest('.o_existing_attachment_cell').remove();\n                        }\n                        return;\n                    }\n                    self.$errorText.replaceWith(QWeb.render('wysiwyg.widgets.image.existing.error', {\n                        views: prevented[id],\n                        widget: self,\n                    }));\n                });\n            }\n        });\n    },\n    /**\n     * @private\n     */\n    _onURLInputChange: function () {\n        const inputValue = this.$urlInput.val().split('?')[0];\n        var emptyValue = (inputValue === '');\n\n        var isURL = /^.+\\..+$/.test(inputValue); // TODO improve\n        var isImage = _.any(this.IMAGE_EXTENSIONS, function (format) {\n            return inputValue.endsWith(format);\n        });\n\n        this._updateAddUrlUi(emptyValue, isURL, isImage);\n    },\n    /**\n     * @private\n     */\n    _onUploadButtonClick: function () {\n        this.$fileInput.click();\n    },\n    /**\n     * @private\n     */\n    _onUploadURLButtonClick: function () {\n        if (this.$urlInput.is('.o_we_horizontal_collapse')) {\n            this.$urlInput.removeClass('o_we_horizontal_collapse');\n            this.$addUrlButton.attr('disabled', 'disabled');\n            return;\n        }\n        return this._mutex.exec(this._addUrl.bind(this));\n    },\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _addUrl: function () {\n        var self = this;\n        return this._rpc({\n            route: '/web_editor/attachment/add_url',\n            params: {\n                'url': this.$urlInput.val(),\n                'res_id': this.options.res_id,\n                'res_model': this.options.res_model,\n            },\n        }).then(function (attachment) {\n            self.$urlInput.val('');\n            self._onURLInputChange();\n            self._handleNewAttachment(attachment);\n            if (!self.options.multiImages) {\n                self.trigger_up('save_request');\n            }\n        });\n    },\n    /**\n     * @private\n     */\n    _onLoadMoreClick: function () {\n        this._loadMoreImages();\n    },\n    /**\n     * @override\n     */\n    _onSearchInput: function () {\n        this.attachments = [];\n        this.numberOfAttachmentsToDisplay = this.NUMBER_OF_ATTACHMENTS_TO_DISPLAY;\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets up a progress bar for every file being uploaded in a toast.\n     *\n     * @private\n     * @param {Object[]} files\n     */\n    _setUpProgressToast: async function (files) {\n        this.uploader = new UploadProgressToast(this, files);\n        await this.uploader.appendTo(document.body);\n    },\n});\n\n/**\n * Let users choose an image, including uploading a new image in odoo.\n */\nvar ImageWidget = FileWidget.extend({\n    template: 'wysiwyg.widgets.image',\n    existingAttachmentsTemplate: 'wysiwyg.widgets.image.existing.attachments',\n    events: Object.assign({}, FileWidget.prototype.events, {\n        'change input.o_we_show_optimized': '_onShowOptimizedChange',\n        'change .o_we_search_select': '_onSearchSelect',\n    }),\n    MIN_ROW_HEIGHT: 128,\n\n    /**\n     * @constructor\n     */\n    init: function (parent, media, options) {\n        this.searchService = 'all';\n        this.widgetType = 'image';\n        options = _.extend({\n            accept: 'image/*',\n            mimetypeDomain: [['mimetype', 'in', this.IMAGE_MIMETYPES]],\n        }, options || {});\n        // Binding so we can add/remove it as an addEventListener\n        this._onAttachmentImageLoad = this._onAttachmentImageLoad.bind(this);\n        this._super(parent, media, options);\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n        this.el.addEventListener('load', this._onAttachmentImageLoad, true);\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        this.el.removeEventListener('load', this._onAttachmentImageLoad, true);\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async fetchAttachments(number, offset) {\n        if (this.needle && this.searchService !== 'database') {\n            number = this.MAX_DB_ATTACHMENTS;\n            offset = 0;\n        }\n        const result = await this._super(number, offset);\n        // Color-substitution for dynamic SVG attachment\n        const primaryColors = {};\n        for (let color = 1; color <= 5; color++) {\n            primaryColors[color] = getCSSVariableValue('o-color-' + color);\n        }\n        this.attachments.forEach(attachment => {\n            if (attachment.image_src.startsWith('/')) {\n                const newURL = new URL(attachment.image_src, window.location.origin);\n                // Set the main colors of dynamic SVGs to o-color-1~5\n                if (attachment.image_src.startsWith('/web_editor/shape/')) {\n                    newURL.searchParams.forEach((value, key) => {\n                        const match = key.match(/^c([1-5])$/);\n                        if (match) {\n                            newURL.searchParams.set(key, primaryColors[match[1]]);\n                        }\n                    })\n                } else {\n                    // Set height so that db images load faster\n                    newURL.searchParams.set('height', 2 * this.MIN_ROW_HEIGHT);\n                }\n                attachment.thumbnail_src = newURL.pathname + newURL.search;\n            }\n        });\n        if (this.needle && this.options.useMediaLibrary) {\n            try {\n                const response = await this._rpc({\n                    route: '/web_editor/media_library_search',\n                    params: {\n                        'query': this.needle,\n                        'offset': this.libraryMedia.length,\n                    },\n                });\n                const newMedia = response.media;\n                this.nbMediaResults = response.results;\n                this.libraryMedia.push(...newMedia);\n            } catch (e) {\n                // Either API endpoint doesn't exist or is misconfigured.\n                console.error(`Couldn't reach API endpoint.`);\n            }\n        }\n        return result;\n    },\n    /**\n     * @override\n     */\n    hasContent() {\n        if (this.searchService === 'all') {\n            return this._super(...arguments) || this.libraryMedia.length;\n        } else if (this.searchService === 'media-library') {\n            return !!this.libraryMedia.length;\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _updateAddUrlUi: function (emptyValue, isURL, isImage) {\n        this._super.apply(this, arguments);\n        const warning = isURL && !isImage;\n        this.$urlWarning.toggleClass('d-none', !warning);\n        this.$addUrlButton.prop('disabled', warning || !isURL);\n        this.$urlSuccess.toggleClass('d-none', warning || !isURL);\n    },\n    /**\n     * @override\n     */\n    _renderThumbnails: function () {\n        const alreadyLoaded = this.$('.o_existing_attachment_cell[data-loaded=\"true\"]');\n        this._super(...arguments);\n        // Hide images until they're loaded\n        this.$('.o_existing_attachment_cell').addClass('d-none');\n        // Replace images that had been previously loaded if any to prevent scroll resetting to top\n        alreadyLoaded.each((index, el) => {\n            const toReplace = this.$(`.o_existing_attachment_cell[data-id=\"${el.dataset.id}\"], .o_existing_attachment_cell[data-media-id=\"${el.dataset.mediaId}\"]`);\n            if (toReplace.length) {\n                toReplace.replaceWith(el);\n            }\n        });\n        this._toggleOptimized(this.$('input.o_we_show_optimized')[0].checked);\n        // Placeholders have a 3:2 aspect ratio like most photos.\n        const placeholderWidth = 3 / 2 * this.MIN_ROW_HEIGHT;\n        this.$('.o_we_attachment_placeholder').css({\n            flexGrow: placeholderWidth,\n            flexBasis: placeholderWidth,\n        });\n        if (this.needle && ['media-library', 'all'].includes(this.searchService)) {\n            const noMoreImgToLoad = this.libraryMedia.length === this.nbMediaResults;\n            const noLoadMoreButton = noMoreImgToLoad && this.libraryMedia.length <= 15;\n            this.$('.o_load_done_msg').toggleClass('d-none', noLoadMoreButton || !noMoreImgToLoad);\n            this.$('.o_load_more').toggleClass('d-none', noMoreImgToLoad);\n        }\n    },\n    /**\n     * @override\n     */\n    _renderExisting: function (attachments) {\n        if (this.needle && this.searchService !== 'database') {\n            attachments = attachments.slice(0, this.MAX_DB_ATTACHMENTS);\n        }\n        return QWeb.render(this.existingAttachmentsTemplate, {\n            attachments: attachments,\n            libraryMedia: this.libraryMedia,\n            widget: this,\n        });\n    },\n    /**\n     * @private\n     *\n     * @param {boolean} value whether to toggle optimized attachments on or off\n     */\n    _toggleOptimized: function (value) {\n        this.$('.o_we_attachment_optimized').each((i, cell) => cell.style.setProperty('display', value ? null : 'none', 'important'));\n    },\n    /**\n     * @override\n     */\n    _highlightSelected: function () {\n        this._super(...arguments);\n        this.selectedMedia.forEach(media => {\n            this.$(`.o_existing_attachment_cell[data-media-id=${media.id}]`)\n                .addClass(\"o_we_attachment_selected\");\n        });\n    },\n    /**\n     * @override\n     */\n    _getAttachmentsDomain(needle) {\n        const domain = this._super(...arguments);\n\n        // Optimized images (meaning they are related to an `original_id`) can\n        // only be shown in debug mode as the toggler to make those images\n        // appear is hidden when not in debug mode.\n        // There is thus no point to fetch those optimized images outside debug\n        // mode. Worst, it leads to bugs: it might fetch only optimized images\n        // when clicking on \"load more\" which will look like it's bugged as no\n        // images will appear on screen (they all will be hidden).\n        if (!config.isDebug()) {\n            const subDomain = [false];\n\n            // Particular exception: if the edited image is an optimized\n            // image, we need to fetch it too so it's displayed as the\n            // selected image when opening the media dialog.\n            // We might get a few more optimized image than necessary if the\n            // original image has multiple optimized images but it's not a\n            // big deal.\n            const originalId = this.$media.length && this.$media[0].dataset.originalId;\n            if (originalId) {\n                subDomain.push(originalId);\n            }\n\n            domain.push(['original_id', 'in', subDomain]);\n        }\n\n        return domain;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _onAttachmentImageLoad: async function (ev) {\n        const img = ev.target;\n        const cell = img.closest('.o_existing_attachment_cell');\n        if (!cell) {\n            return;\n        }\n        if (cell.dataset.mediaId && !img.src.startsWith('blob')) {\n            const mediaUrl = img.src;\n            try {\n                const response = await fetch(mediaUrl);\n                if (response.headers.get('content-type') === 'image/svg+xml') {\n                    let svg = await response.text();\n                    const fileName = mediaUrl.split('/').pop();\n                    const dynamicColors = {};\n                    const combinedColorsRegex = new RegExp(Object.values(DEFAULT_PALETTE).join('|'), 'gi');\n                    svg = svg.replace(combinedColorsRegex, match => {\n                        const colorId = Object.keys(DEFAULT_PALETTE).find(key => DEFAULT_PALETTE[key] === match.toUpperCase());\n                        const colorKey = 'c' + colorId\n                        dynamicColors[colorKey] = getCSSVariableValue('o-color-' + colorId);\n                        return dynamicColors[colorKey];\n                    });\n                    if (Object.keys(dynamicColors).length) {\n                        const file = new File([svg], fileName, {\n                            type: \"image/svg+xml\",\n                        });\n                        img.src = URL.createObjectURL(file);\n                        const media = this.libraryMedia.find(media => media.id === parseInt(cell.dataset.mediaId));\n                        if (media) {\n                            media.isDynamicSVG = true;\n                            media.dynamicColors = dynamicColors;\n                        }\n                        // We changed the src: wait for the next load event to do the styling\n                        return;\n                    }\n                }\n            } catch (e) {\n                console.error('CORS is misconfigured on the API server, image will be treated as non-dynamic.');\n            }\n        }\n        let aspectRatio = img.naturalWidth / img.naturalHeight;\n        // Special case for SVGs with no instrinsic sizes on firefox\n        // See https://github.com/whatwg/html/issues/3510#issuecomment-369982529\n        if (img.naturalHeight === 0) {\n            img.width = 1000;\n            // Position fixed so that the image doesn't affect layout while rendering\n            img.style.position = 'fixed';\n            // Make invisible so the image doesn't briefly appear on the screen\n            img.style.opacity = '0';\n            // Image needs to be in the DOM for dimensions to be correct after render\n            const originalParent = img.parentElement;\n            document.body.appendChild(img);\n\n            aspectRatio = img.width / img.height;\n            originalParent.appendChild(img);\n            img.removeAttribute('width');\n            img.style.removeProperty('position');\n            img.style.removeProperty('opacity');\n        }\n        const width = aspectRatio * this.MIN_ROW_HEIGHT;\n        cell.style.flexGrow = width;\n        cell.style.flexBasis = `${width}px`;\n        cell.classList.remove('d-none');\n        cell.classList.add('d-flex');\n        cell.dataset.loaded = 'true';\n    },\n    /**\n     * @override\n     */\n    _onShowOptimizedChange: function (ev) {\n        this._toggleOptimized(ev.target.checked);\n    },\n    /**\n     * @override\n     */\n    _onSearchSelect: function (ev) {\n        const {value} = ev.target;\n        this.searchService = value;\n        this.$('.o_we_search').trigger('input');\n    },\n    /**\n     * @private\n     */\n    _onSearchInput: function (ev) {\n        this.libraryMedia = [];\n        this._super(...arguments);\n        this.$('.o_we_search_select').removeClass('d-none');\n    },\n    /**\n     * @override\n     */\n    _clear: function (type) {\n        // Not calling _super: we don't want to call the document widget's _clear method on images\n        var allImgClasses = /(^|\\s+)(img|img-\\S*|o_we_custom_image|rounded-circle|rounded|thumbnail|shadow|w-25|w-50|w-75|w-100|o_modified_image_to_save)(?=\\s|$)/g;\n        this.media.className = this.media.className && this.media.className.replace(allImgClasses, ' ');\n    },\n});\n\n\n/**\n * Let users choose a document, including uploading a new document in odoo.\n */\nvar DocumentWidget = FileWidget.extend({\n    template: 'wysiwyg.widgets.document',\n    existingAttachmentsTemplate: 'wysiwyg.widgets.document.existing.attachments',\n\n    /**\n     * @constructor\n     */\n    init: function (parent, media, options) {\n        options = _.extend({\n            accept: '*/*',\n            mimetypeDomain: [['mimetype', 'not in', this.IMAGE_MIMETYPES]],\n        }, options || {});\n        this._super(parent, media, options);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getAttachmentsDomain: function (needle) {\n        var domain = this._super.apply(this, arguments);\n        // the assets should not be part of the documents\n        return domain.concat('!', utils.assetsDomain());\n    },\n});\n\n/**\n * Let users choose a font awesome icon, support all font awesome loaded in the\n * css files.\n */\nvar IconWidget = SearchableMediaWidget.extend({\n    template: 'wysiwyg.widgets.font-icons',\n    events: _.extend({}, SearchableMediaWidget.prototype.events || {}, {\n        'click .font-icons-icon': '_onIconClick',\n    }),\n\n    /**\n     * @constructor\n     */\n    init: function (parent, media) {\n        this._super.apply(this, arguments);\n\n        fonts.computeFonts();\n        this.iconsParser = fonts.fontIcons;\n        this.alias = _.flatten(_.map(this.iconsParser, function (data) {\n            return data.alias;\n        }));\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.$icons = this.$('.font-icons-icon');\n        var classes = (this.media && this.media.className || '').split(/\\s+/);\n        for (var i = 0; i < classes.length; i++) {\n            var cls = classes[i];\n            if (_.contains(this.alias, cls)) {\n                this.selectedIcon = cls;\n                this.initialIcon = cls;\n                this._highlightSelectedIcon();\n            }\n        }\n\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    save: async function () {\n        var style = this.$media.attr('style') || '';\n        var iconFont = this._getFont(this.selectedIcon) || {base: 'fa', font: ''};\n        if (!this.$media.is('span, i')) {\n            var $span = $('<span/>');\n            if (this.$media.length) {\n                // Make sure jquery data() is clean by signaling the removal\n                // (e.g. website wants to remove SnippetEditor references from\n                // the data).\n                // TODO make sure copying the data is in fact useful at all, but\n                // in stable it did not feel safe to remove anyway.\n                //\n                // Note: done with an array of promises filled by the event\n                // handler instead of a Promise created here to be resolved by\n                // the event handler as the event handler does not necessarily\n                // exists (in simple HTML fields for example).\n                const data = { proms: [] };\n                this.$media.trigger('before_replace_target', data);\n                await Promise.all(data.proms);\n                $span.data(this.$media.data());\n            }\n            this.$media = $span;\n            this.media = this.$media[0];\n            style = style.replace(/\\s*width:[^;]+/, '');\n        }\n        this.$media.removeClass(this.initialIcon).addClass([iconFont.base, iconFont.font]);\n        this.$media.attr('style', style || null);\n        return Promise.resolve(this.media);\n    },\n    /**\n     * @override\n     */\n    search: function (needle) {\n        var iconsParser = this.iconsParser;\n        if (needle && needle.length) {\n            iconsParser = [];\n            _.filter(this.iconsParser, function (data) {\n                var cssData = _.filter(data.cssData, function (cssData) {\n                    return _.find(cssData.names, function (alias) {\n                        return alias.indexOf(needle) >= 0;\n                    });\n                });\n                if (cssData.length) {\n                    iconsParser.push({\n                        base: data.base,\n                        cssData: cssData,\n                    });\n                }\n            });\n        }\n        this.$('div.font-icons-icons').html(\n            QWeb.render('wysiwyg.widgets.font-icons.icons', {iconsParser: iconsParser, widget: this})\n        );\n        return Promise.resolve();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _clear: function () {\n        var allFaClasses = /(^|\\s)(fa|(text-|bg-|fa-)\\S*|rounded-circle|rounded|thumbnail|img-thumbnail|shadow)(?=\\s|$)/g;\n        this.media.className = this.media.className && this.media.className.replace(allFaClasses, ' ');\n    },\n    /**\n     * @private\n     */\n    _getFont: function (classNames) {\n        if (!(classNames instanceof Array)) {\n            classNames = (classNames || \"\").split(/\\s+/);\n        }\n        var fontIcon, cssData;\n        for (var k = 0; k < this.iconsParser.length; k++) {\n            fontIcon = this.iconsParser[k];\n            for (var s = 0; s < fontIcon.cssData.length; s++) {\n                cssData = fontIcon.cssData[s];\n                if (_.intersection(classNames, cssData.names).length) {\n                    return {\n                        base: fontIcon.base,\n                        parser: fontIcon.parser,\n                        font: cssData.names[0],\n                    };\n                }\n            }\n        }\n        return null;\n    },\n    /**\n     * @private\n     */\n    _highlightSelectedIcon: function () {\n        var self = this;\n        this.$icons.removeClass('o_we_attachment_selected');\n        this.$icons.filter(function (i, el) {\n            return _.contains($(el).data('alias').split(','), self.selectedIcon);\n        }).addClass('o_we_attachment_selected');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onIconClick: function (ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n\n        this.selectedIcon = $(ev.currentTarget).data('id');\n        this._highlightSelectedIcon();\n        this.trigger_up('save_request');\n    },\n});\n\n/**\n * Let users choose a video, support embed iframe.\n */\nvar VideoWidget = MediaWidget.extend({\n    template: 'wysiwyg.widgets.video',\n    events: _.extend({}, MediaWidget.prototype.events || {}, {\n        'change .o_video_dialog_options input': '_onUpdateVideoOption',\n        'input textarea#o_video_text': '_onVideoCodeInput',\n        'change textarea#o_video_text': '_onVideoCodeChange',\n        'click .o_sample_video': '_onSampleVideoClick',\n    }),\n\n    /**\n     * @constructor\n     */\n    init: function (parent, media, options) {\n        this._super.apply(this, arguments);\n        this.isForBgVideo = !!options.isForBgVideo;\n        this._onVideoCodeInput = _.debounce(this._onVideoCodeInput, 1000);\n        // list of videoIds from vimeo.\n        this._vimeoPreviewIds = options.vimeoPreviewIds;\n    },\n    /**\n     * @override\n     */\n    start: function () {\n        this.$content = this.$('.o_video_dialog_iframe');\n\n        if (this.media) {\n            var $media = $(this.media);\n            var src = $media.data('oe-expression') || $media.data('src') || ($media.is('iframe') ? $media.attr('src') : '') || '';\n            this.$('textarea#o_video_text').val(src);\n\n            this.$('input#o_video_autoplay').prop('checked', src.indexOf('autoplay=1') >= 0);\n            this.$('input#o_video_hide_controls').prop('checked', src.indexOf('controls=0') >= 0);\n            this.$('input#o_video_loop').prop('checked', src.indexOf('loop=1') >= 0);\n            this.$('input#o_video_hide_fullscreen').prop('checked', src.indexOf('fs=0') >= 0);\n            this.$('input#o_video_hide_yt_logo').prop('checked', src.indexOf('modestbranding=1') >= 0);\n            this.$('input#o_video_hide_dm_logo').prop('checked', src.indexOf('ui-logo=0') >= 0);\n            this.$('input#o_video_hide_dm_share').prop('checked', src.indexOf('sharing-enable=0') >= 0);\n\n            this._updateVideo();\n        }\n\n        // loads the thumbnail of vimeo video previews.\n        this.$('.o_sample_video').each((index, node) => {\n            const $node = $(node);\n            const videoId = $node.attr('data-vimeo');\n            if (!videoId) {\n                return;\n            }\n            fetch(`https://vimeo.com/api/oembed.json?url=http%3A//vimeo.com/${encodeURIComponent(videoId)}`)\n                .then(response=>response.json())\n                .then((response) => {\n                    $node.append($('<img>', {\n                        src: response.thumbnail_url,\n                        class: 'mw-100 mh-100 p-1',\n                    }));\n                });\n        });\n\n        return this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    save: function () {\n        this._updateVideo();\n        const videoSrc = this.$content.attr('src');\n        if (this.isForBgVideo) {\n            return Promise.resolve({bgVideoSrc: videoSrc});\n        }\n        if (this.$('.o_video_dialog_iframe').is('iframe') && videoSrc) {\n            this.$media = this.getWrappedIframe(videoSrc);\n            this.media = this.$media[0];\n        }\n        return Promise.resolve(this.media);\n    },\n\n    /**\n     * Get an iframe wrapped for the website builder.\n     *\n     * @param {string} src The video url.\n     */\n    getWrappedIframe: function (src) {\n        return $(\n            '<div class=\"media_iframe_video\" data-oe-expression=\"' + src + '\">' +\n                '<div class=\"css_editable_mode_display\">&nbsp;</div>' +\n                '<div class=\"media_iframe_video_size\" contenteditable=\"false\">&nbsp;</div>' +\n                '<iframe src=\"' + src + '\" frameborder=\"0\" contenteditable=\"false\" allowfullscreen=\"allowfullscreen\"></iframe>' +\n            '</div>'\n        );\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _clear: function () {\n        if (this.media.dataset.src) {\n            try {\n                delete this.media.dataset.src;\n            } catch (e) {\n                this.media.dataset.src = undefined;\n            }\n        }\n        var allVideoClasses = /(^|\\s)media_iframe_video(\\s|$)/g;\n        var isVideo = this.media.className && this.media.className.match(allVideoClasses);\n        if (isVideo) {\n            this.media.className = this.media.className.replace(allVideoClasses, ' ');\n            this.media.innerHTML = '';\n        }\n    },\n    /**\n     * Creates a video node according to the given URL and options. If not\n     * possible, returns an error code.\n     *\n     * @private\n     * @param {string} url\n     * @param {Object} options\n     * @returns {Object}\n     *          $video -> the created video jQuery node\n     *          type -> the type of the created video\n     *          errorCode -> if defined, either '0' for invalid URL or '1' for\n     *              unsupported video provider\n     */\n    _createVideoNode: function (url, options) {\n        options = options || {};\n        const videoData = this._getVideoURLData(url, options);\n        if (videoData.error) {\n            return {errorCode: 0};\n        }\n        if (!videoData.type) {\n            return {errorCode: 1};\n        }\n        const $video = $('<iframe>').width(1280).height(720)\n            .attr('frameborder', 0)\n            .attr('src', videoData.embedURL)\n            .addClass('o_video_dialog_iframe');\n\n        return {$video: $video, type: videoData.type};\n    },\n    /**\n     * Updates the video preview according to video code and enabled options.\n     *\n     * @private\n     */\n    _updateVideo: function () {\n        // Reset the feedback\n        this.$content.empty();\n        this.$('#o_video_form_group').removeClass('o_has_error o_has_success').find('.form-control, .custom-select').removeClass('is-invalid is-valid');\n        this.$('.o_video_dialog_options div').addClass('d-none');\n\n        // Check video code\n        var $textarea = this.$('textarea#o_video_text');\n        var code = $textarea.val().trim();\n        if (!code) {\n            return;\n        }\n\n        // Detect if we have an embed code rather than an URL\n        var embedMatch = code.match(/(src|href)=[\"']?([^\"']+)?/);\n        if (embedMatch && embedMatch[2].length > 0 && embedMatch[2].indexOf('instagram')) {\n            embedMatch[1] = embedMatch[2]; // Instagram embed code is different\n        }\n        var url = embedMatch ? embedMatch[1] : code;\n\n        var query = this._createVideoNode(url, {\n            'autoplay': this.isForBgVideo || this.$('input#o_video_autoplay').is(':checked'),\n            'hide_controls': this.isForBgVideo || this.$('input#o_video_hide_controls').is(':checked'),\n            'loop': this.isForBgVideo || this.$('input#o_video_loop').is(':checked'),\n            'hide_fullscreen': this.isForBgVideo || this.$('input#o_video_hide_fullscreen').is(':checked'),\n            'hide_yt_logo': this.isForBgVideo || this.$('input#o_video_hide_yt_logo').is(':checked'),\n            'hide_dm_logo': this.isForBgVideo || this.$('input#o_video_hide_dm_logo').is(':checked'),\n            'hide_dm_share': this.isForBgVideo || this.$('input#o_video_hide_dm_share').is(':checked'),\n        });\n\n        var $optBox = this.$('.o_video_dialog_options');\n\n        // Show / Hide preview elements\n        this.$el.find('.o_video_dialog_preview_text, .media_iframe_video_size').add($optBox).toggleClass('d-none', !query.$video);\n        // Toggle validation classes\n        this.$el.find('#o_video_form_group')\n            .toggleClass('o_has_error', !query.$video).find('.form-control, .custom-select').toggleClass('is-invalid', !query.$video)\n            .end()\n            .toggleClass('o_has_success', !!query.$video).find('.form-control, .custom-select').toggleClass('is-valid', !!query.$video);\n\n        // Individually show / hide options base on the video provider\n        $optBox.find('div.o_' + query.type + '_option').removeClass('d-none');\n\n        // Hide the entire options box if no options are available or if the\n        // dialog is opened for a background-video\n        $optBox.toggleClass('d-none', this.isForBgVideo || $optBox.find('div:not(.d-none)').length === 0);\n\n        if (query.type === 'youtube') {\n            // Youtube only: If 'hide controls' is checked, hide 'fullscreen'\n            // and 'youtube logo' options too\n            this.$('input#o_video_hide_fullscreen, input#o_video_hide_yt_logo').closest('div').toggleClass('d-none', this.$('input#o_video_hide_controls').is(':checked'));\n        }\n\n        this.error = false;\n        var $content = query.$video;\n        if (!$content) {\n            switch (query.errorCode) {\n                case 0:\n                    this.error = _t(\"The provided url is not valid\");\n                    $content = $('<div/>', {\n                        class: 'alert alert-danger o_video_dialog_iframe mb-2 mt-2',\n                        text: this.error,\n                    });\n                    break;\n                case 1:\n                    this.error = _t(\"The provided url does not reference any supported video\");\n                    $content = $('<div/>', {\n                        class: 'alert alert-warning o_video_dialog_iframe mb-2 mt-2',\n                        text: this.error,\n                    });\n                    break;\n            }\n        }\n        this.$content.replaceWith($content);\n        this.$content = $content;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Called when a video option changes -> Updates the video preview.\n     *\n     * @private\n     */\n    _onUpdateVideoOption: function () {\n        this._updateVideo();\n    },\n    /**\n     * changes the video preview when clicking on the thumbnail of a suggested video\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSampleVideoClick(ev) {\n        const vimeoId = ev.currentTarget.getAttribute('data-vimeo');\n        if (vimeoId) {\n            this.$('#o_video_text').val(`https://player.vimeo.com/video/${encodeURIComponent(vimeoId)}`);\n            this._updateVideo();\n        }\n    },\n    /**\n     * Called when the video code (URL / Iframe) change is confirmed -> Updates\n     * the video preview immediately.\n     *\n     * @private\n     */\n    _onVideoCodeChange: function () {\n        this._updateVideo();\n    },\n    /**\n     * Called when the video code (URL / Iframe) changes -> Updates the video\n     * preview (note: this function is automatically debounced).\n     *\n     * @private\n     */\n    _onVideoCodeInput: function () {\n        this._updateVideo();\n    },\n    /**\n     * Parses a URL and returns the provider type and an emebedable URL.\n     *\n     * @private\n     */\n    _getVideoURLData: function (url, options) {\n        if (!url.match(/^(http:\\/\\/|https:\\/\\/|\\/\\/)[a-z0-9]+([-.]{1}[a-z0-9]+)*\\.[a-z]{2,5}(:[0-9]{1,5})?(\\/.*)?$/i)) {\n            return {\n                error: true,\n                message: 'The provided url is invalid',\n            };\n        }\n        const regexes = {\n            youtube: /^(?:(?:https?:)?\\/\\/)?(?:www\\.)?(?:youtu\\.be\\/|youtube(-nocookie)?\\.com\\/(?:embed\\/|v\\/|watch\\?v=|watch\\?.+&v=))((?:\\w|-){11})(?:\\S+)?$/,\n            instagram: /(.*)instagram.com\\/p\\/(.[a-zA-Z0-9]*)/,\n            vine: /\\/\\/vine.co\\/v\\/(.[a-zA-Z0-9]*)/,\n            vimeo: /\\/\\/(player.)?vimeo.com\\/([a-z]*\\/)*([0-9]{6,11})[?]?.*/,\n            dailymotion: /.+dailymotion.com\\/(video|hub|embed)\\/([^_?]+)[^#]*(#video=([^_&]+))?/,\n            youku: /(.*).youku\\.com\\/(v_show\\/id_|embed\\/)(.+)/,\n        };\n        const matches = _.mapObject(regexes, regex => url.match(regex));\n        const autoplay = options.autoplay ? '?autoplay=1&mute=1' : '?autoplay=0';\n        const controls = options.hide_controls ? '&controls=0' : '';\n        const loop = options.loop ? '&loop=1' : '';\n\n        let embedURL;\n        let type;\n        if (matches.youtube && matches.youtube[2].length === 11) {\n            const fullscreen = options.hide_fullscreen ? '&fs=0' : '';\n            const ytLoop = loop ? loop + `&playlist=${encodeURIComponent(matches.youtube[2])}` : '';\n            const logo = options.hide_yt_logo ? '&modestbranding=1' : '';\n            // The youtube js api is needed for autoplay on mobile. Note: this\n            // was added as a fix, old customers may have autoplay videos\n            // without this, which will make their video autoplay on desktop\n            // but not in mobile (so no behavior change was done in stable,\n            // this should not be migrated).\n            const enablejsapi = options.autoplay ? '&enablejsapi=1' : '';\n            embedURL = `//www.youtube${matches.youtube[1] || ''}.com/embed/${encodeURIComponent(matches.youtube[2])}${autoplay}${enablejsapi}&rel=0${ytLoop}${controls}${fullscreen}${logo}`;\n            type = 'youtube';\n        } else if (matches.instagram && matches.instagram[2].length) {\n            embedURL = `//www.instagram.com/p/${encodeURIComponent(matches.instagram[2])}/embed/`;\n            type = 'instagram';\n        } else if (matches.vine && matches.vine[0].length) {\n            embedURL = `${matches.vine[0]}/embed/simple`;\n            type = 'vine';\n        } else if (matches.vimeo && matches.vimeo[3].length) {\n            const vimeoAutoplay = autoplay.replace('mute', 'muted')\n                .replace('autoplay=1', 'autoplay=1&autopause=0');\n            embedURL = `//player.vimeo.com/video/${encodeURIComponent(matches.vimeo[3])}${vimeoAutoplay}${loop}${controls}`;\n            type = 'vimeo';\n        } else if (matches.dailymotion && matches.dailymotion[2].length) {\n            const videoId = matches.dailymotion[2].replace('video/', '');\n            const logo = options.hide_dm_logo ? '&ui-logo=0' : '';\n            const share = options.hide_dm_share ? '&sharing-enable=0' : '';\n            embedURL = `//www.dailymotion.com/embed/video/${encodeURIComponent(videoId)}${autoplay}${controls}${logo}${share}`;\n            type = 'dailymotion';\n        } else if (matches.youku && matches.youku[3].length) {\n            const videoId = matches.youku[3].indexOf('.html?') >= 0 ? matches.youku[3].substring(0, matches.youku[3].indexOf('.html?')) : matches.youku[3];\n            embedURL = `//player.youku.com/embed/${videoId}`;\n            type = 'youku';\n        }\n\n        return {type: type, embedURL: embedURL};\n    },\n});\n\nreturn {\n    MediaWidget: MediaWidget,\n    SearchableMediaWidget: SearchableMediaWidget,\n    FileWidget: FileWidget,\n    ImageWidget: ImageWidget,\n    DocumentWidget: DocumentWidget,\n    IconWidget: IconWidget,\n    VideoWidget: VideoWidget,\n};\n});\n", "odoo.define('wysiwyg.widgets.MediaDialog', function (require) {\n'use strict';\n\nvar core = require('web.core');\nvar MediaModules = require('wysiwyg.widgets.media');\nvar Dialog = require('wysiwyg.widgets.Dialog');\n\nvar _t = core._t;\n\n/**\n * Lets the user select a media. The media can be existing or newly uploaded.\n *\n * The media can be one of the following types: image, document, video or\n * font awesome icon (only existing icons).\n *\n * The user may change a media into another one depending on the given options.\n */\nvar MediaDialog = Dialog.extend({\n    template: 'wysiwyg.widgets.media',\n    xmlDependencies: Dialog.prototype.xmlDependencies.concat(\n        ['/web_editor/static/src/xml/wysiwyg.xml']\n    ),\n    events: _.extend({}, Dialog.prototype.events, {\n        'click #editor-media-image-tab': '_onClickImageTab',\n        'click #editor-media-document-tab': '_onClickDocumentTab',\n        'click #editor-media-icon-tab': '_onClickIconTab',\n        'click #editor-media-video-tab': '_onClickVideoTab',\n    }),\n    custom_events: _.extend({}, Dialog.prototype.custom_events || {}, {\n        save_request: '_onSaveRequest',\n        show_parent_dialog_request: '_onShowRequest',\n        hide_parent_dialog_request: '_onHideRequest',\n    }),\n\n    /**\n     * @constructor\n     * @param {Element} media\n     */\n    init: function (parent, options, media) {\n        var $media = $(media);\n        media = $media[0];\n        this.media = media;\n\n        options = _.extend({}, options);\n        var onlyImages = options.onlyImages || this.multiImages || (media && ($media.parent().data('oeField') === 'image' || $media.parent().data('oeType') === 'image'));\n        options.noDocuments = onlyImages || options.noDocuments;\n        options.noIcons = onlyImages || options.noIcons;\n        options.noVideos = onlyImages || options.noVideos;\n\n        this._super(parent, _.extend({}, {\n            title: _t(\"Select a Media\"),\n            save_text: _t(\"Add\"),\n        }, options));\n\n        if (!options.noImages) {\n            this.imageWidget = new MediaModules.ImageWidget(this, media, options);\n        }\n        if (!options.noDocuments) {\n            this.documentWidget = new MediaModules.DocumentWidget(this, media, options);\n        }\n        if (!options.noIcons) {\n            this.iconWidget = new MediaModules.IconWidget(this, media, options);\n        }\n        if (!options.noVideos) {\n            this.videoWidget = new MediaModules.VideoWidget(this, media, options);\n        }\n\n        if (this.imageWidget && $media.is('img')) {\n            this.activeWidget = this.imageWidget;\n        } else if (this.documentWidget && $media.is('a.o_image')) {\n            this.activeWidget = this.documentWidget;\n        } else if (this.videoWidget && $media.is('.media_iframe_video, .o_bg_video_iframe')) {\n            this.activeWidget = this.videoWidget;\n        } else if (this.iconWidget && $media.is('span, i')) {\n            this.activeWidget = this.iconWidget;\n        } else {\n            this.activeWidget = [this.imageWidget, this.documentWidget, this.videoWidget, this.iconWidget].find(w => !!w);\n        }\n        this.initiallyActiveWidget = this.activeWidget;\n    },\n    /**\n     * Adds the appropriate class to the current modal and appends the media\n     * widgets to their respective tabs.\n     *\n     * @override\n     */\n    start: function () {\n        var promises = [this._super.apply(this, arguments)];\n        this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');\n\n        if (this.imageWidget) {\n            promises.push(this.imageWidget.appendTo(this.$(\"#editor-media-image\")));\n        }\n        if (this.documentWidget) {\n            promises.push(this.documentWidget.appendTo(this.$(\"#editor-media-document\")));\n        }\n        if (this.iconWidget) {\n            promises.push(this.iconWidget.appendTo(this.$(\"#editor-media-icon\")));\n        }\n        if (this.videoWidget) {\n            promises.push(this.videoWidget.appendTo(this.$(\"#editor-media-video\")));\n        }\n\n        this.opened(() => this.$('input.o_we_search:visible:first').focus());\n\n        return Promise.all(promises);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns whether the document widget is currently active.\n     *\n     * @returns {boolean}\n     */\n    isDocumentActive: function () {\n        return this.activeWidget === this.documentWidget;\n    },\n    /**\n     * Returns whether the icon widget is currently active.\n     *\n     * @returns {boolean}\n     */\n    isIconActive: function () {\n        return this.activeWidget === this.iconWidget;\n    },\n    /**\n     * Returns whether the image widget is currently active.\n     *\n     * @returns {boolean}\n     */\n    isImageActive: function () {\n        return this.activeWidget === this.imageWidget;\n    },\n    /**\n     * Returns whether the video widget is currently active.\n     *\n     * @returns {boolean}\n     */\n    isVideoActive: function () {\n        return this.activeWidget === this.videoWidget;\n    },\n    /**\n     * Saves the currently selected media from the currently active widget.\n     *\n     * The save event data `final_data` will be one Element in general, but it\n     * will be an Array of Element if `multiImages` is set.\n     *\n     * @override\n     */\n    save: function () {\n        var self = this;\n        var _super = this._super;\n        var args = arguments;\n        return this.activeWidget.save().then(function (data) {\n            if (self.activeWidget.error) {\n                self.displayNotification({\n                    type: 'danger',\n                    message: self.activeWidget.error,\n                });\n                return;\n            }\n            if (self.activeWidget !== self.initiallyActiveWidget) {\n                self._clearWidgets();\n            }\n            // Restore classes if the media was replaced (when changing type)\n            if (self.media !== data) {\n                var oldClasses = self.media && _.toArray(self.media.classList);\n                if (oldClasses) {\n                    data.className = _.union(_.toArray(data.classList), oldClasses).join(' ');\n                }\n            }\n            self.final_data = data;\n            _super.apply(self, args);\n            $(data).trigger('content_changed');\n            $(data).trigger('replace_target', data);\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Call clear on all the widgets except the activeWidget.\n     * We clear because every widgets are modifying the \"media\" element.\n     * All widget have the responsibility to clear a previous element that\n     * was created from them.\n     */\n    _clearWidgets: function () {\n        [   this.imageWidget,\n            this.documentWidget,\n            this.iconWidget,\n            this.videoWidget\n        ].forEach( (widget) => {\n            if (widget !== this.activeWidget) {\n                widget && widget.clear();\n            }\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Sets the document widget as the active widget.\n     *\n     * @private\n     */\n    _onClickDocumentTab: function () {\n        this.activeWidget = this.documentWidget;\n    },\n    /**\n     * Sets the icon widget as the active widget.\n     *\n     * @private\n     */\n    _onClickIconTab: function () {\n        this.activeWidget = this.iconWidget;\n    },\n    /**\n     * Sets the image widget as the active widget.\n     *\n     * @private\n     */\n    _onClickImageTab: function () {\n        this.activeWidget = this.imageWidget;\n    },\n    /**\n     * Sets the video widget as the active widget.\n     *\n     * @private\n     */\n    _onClickVideoTab: function () {\n        this.activeWidget = this.videoWidget;\n    },\n    /**\n     * Handles hide request from child widgets.\n     *\n     * This is for usability, to allow hiding the modal for example when another\n     * smaller modal would be displayed on top.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onHideRequest: function (ev) {\n        this.$modal.addClass('d-none');\n    },\n    /**\n     * Handles save request from the child widgets.\n     *\n     * This is for usability, to allow the user to save from other ways than\n     * click on the modal button, such as double clicking a media to select it.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSaveRequest: function (ev) {\n        ev.stopPropagation();\n        this.save();\n    },\n    /**\n     * Handles show request from the child widgets.\n     *\n     * This is for usability, it is the counterpart of @see _onHideRequest.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onShowRequest: function (ev) {\n        this.$modal.removeClass('d-none');\n    },\n});\n\nreturn MediaDialog;\n});\n", "/** @odoo-module */\nimport Widget from \"web.Widget\";\nimport {qweb} from 'web.core';\n\nexport const UploadProgressToast = Widget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/wysiwyg.xml'],\n    template: 'wysiwyg.widgets.upload.progress_toast',\n    events: {\n        'click .o_notification_close': '_onCloseClick',\n    },\n\n    /**\n     * @override\n     */\n    init(parent, files) {\n        this._super(...arguments);\n        this.files = files;\n    },\n    /**\n     * @override\n     */\n    start() {\n        this.$progress = $('<div/>');\n        _.each(this.files, (file, index) => {\n            let fileSize = file.size;\n            if (!fileSize) {\n                fileSize = null;\n            } else if (fileSize < 1024) {\n                fileSize = fileSize.toFixed(2) + \" bytes\";\n            } else if (fileSize < 1048576) {\n                fileSize = (fileSize / 1024).toFixed(2) + \" KB\";\n            } else {\n                fileSize = (fileSize / 1048576).toFixed(2) + \" MB\";\n            }\n\n            this.$progress.append(qweb.render('wysiwyg.widgets.upload.progressbar', {\n                fileId: index,\n                fileName: file.name,\n                fileSize: fileSize,\n            }));\n        });\n        this.$el.find('.o_notification_content').append(this.$progress);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Calls a RPC and shows its progress status.\n     *\n     * @public\n     * @param {Number} delay time to wait before closing the toast\n     */\n     close(delay = 0) {\n        window.setTimeout(() => {\n            this.el.querySelector('.fade').classList.remove('show');\n            window.setTimeout(() => {\n                this.destroy();\n            }, 150);\n        }, delay);\n    },\n    /**\n     * Calls a RPC and shows its progress status.\n     *\n     * @param {Object} params regular `_rpc()` parameters\n     * @param {integer} index file index to retrieve its related progress bar\n     * @returns {Promise}\n     */\n    async rpcShowProgress(params, index) {\n        let $progressBar = this.$el.find(`.js_progressbar_${index}`);\n        try {\n            const xhr = new XMLHttpRequest();\n            xhr.upload.addEventListener('progress', ev => {\n                const prcComplete = ev.loaded / ev.total * 100;\n                $progressBar.find('.progress-bar').css({\n                    width: Math.floor(prcComplete) + '%',\n                }).text(prcComplete.toFixed(2) + '%');\n            });\n            xhr.upload.addEventListener('load', function () {\n                // Don't show yet success as backend code only starts now\n                $progressBar.find('.progress-bar').css({width: '100%'}).text('100%');\n            });\n            const attachment = await this._rpc(params, {xhr});\n            $progressBar.find('.fa-spinner, .progress').addClass('d-none');\n            if (attachment.error) {\n                this.hasError = true;\n                $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none');\n                $progressBar.find('.js_progressbar_txt .text-danger .o_we_error_text').text(attachment.error);\n            } else {\n                $progressBar.find('.js_progressbar_txt .text-success').removeClass('d-none');\n            }\n            return attachment;\n        } catch (error) {\n            this.hasError = true;\n            $progressBar.find('.fa-spinner, .progress').addClass('d-none');\n            $progressBar.find('.js_progressbar_txt .text-danger').removeClass('d-none');\n            throw error;\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onCloseClick() {\n        this.close();\n    },\n});\n", "odoo.define('wysiwyg.widgets', function (require) {\n'use strict';\n\nvar Dialog = require('wysiwyg.widgets.Dialog');\nvar AltDialog = require('wysiwyg.widgets.AltDialog');\nvar MediaDialog = require('wysiwyg.widgets.MediaDialog');\nvar LinkDialog = require('wysiwyg.widgets.LinkDialog');\nvar LinkTools = require('wysiwyg.widgets.LinkTools');\nvar ImageCropWidget = require('wysiwyg.widgets.ImageCropWidget');\nconst LinkPopoverWidget = require('@web_editor/js/wysiwyg/widgets/link_popover_widget')[Symbol.for(\"default\")];\nconst {ColorpickerDialog} = require('web.Colorpicker');\n\nvar media = require('wysiwyg.widgets.media');\n\nreturn {\n    Dialog: Dialog,\n    AltDialog: AltDialog,\n    MediaDialog: MediaDialog,\n    LinkDialog: LinkDialog,\n    LinkTools: LinkTools,\n    ImageCropWidget: ImageCropWidget,\n    LinkPopoverWidget: LinkPopoverWidget,\n    ColorpickerDialog: ColorpickerDialog,\n\n    MediaWidget: media.MediaWidget,\n    SearchableMediaWidget: media.SearchableMediaWidget,\n    FileWidget: media.FileWidget,\n    ImageWidget: media.ImageWidget,\n    DocumentWidget: media.DocumentWidget,\n    IconWidget: media.IconWidget,\n    VideoWidget: media.VideoWidget,\n};\n});\n", "odoo.define('web_editor.snippet.editor', function (require) {\n'use strict';\n\nvar concurrency = require('web.concurrency');\nvar core = require('web.core');\nvar Dialog = require('web.Dialog');\nvar dom = require('web.dom');\nconst {Markup, sprintf} = require('web.utils');\nvar Widget = require('web.Widget');\nvar options = require('web_editor.snippets.options');\nconst {ColorPaletteWidget} = require('web_editor.ColorPalette');\nconst SmoothScrollOnDrag = require('web/static/src/js/core/smooth_scroll_on_drag.js');\nconst {getCSSVariableValue, shouldEditableMediaBeEditable} = require('web_editor.utils');\nconst QWeb = core.qweb;\n\nvar _t = core._t;\n\n// jQuery extensions\n$.extend($.expr[':'], {\n    o_editable: function (node, i, m) {\n        while (node) {\n            if (node.className && _.isString(node.className)) {\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\n$.fn.extend({\n    focusIn: function () {\n        if (this.length) {\n            const selection = document.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 = document.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 = document.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 = document.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\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 = Widget.extend({\n    template: 'web_editor.snippet_overlay',\n    xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],\n    events: {\n        'click .oe_snippet_remove': '_onRemoveClick',\n        'wheel': '_onMouseWheel',\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 {Widget} 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.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\"]') && !this.$target.hasClass('oe_unremovable');\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            const smoothScrollOptions = this.options.getScrollOptions({\n                jQueryDraggableOptions: {\n                    cursorAt: {\n                        left: 10,\n                        top: 10\n                    },\n                    handle: '.o_move_handle',\n                    helper: () => {\n                        var $clone = this.$el.clone().css({width: '24px', height: '24px', border: 0});\n                        $clone.appendTo(this.$body).removeClass('d-none');\n                        return $clone;\n                    },\n                    start: this._onDragAndDropStart.bind(this),\n                    stop: (...args) => {\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                },\n            });\n            const modalAncestorEl = this.$target[0].closest('.modal');\n            const $scrollable = modalAncestorEl && $(modalAncestorEl)\n                || (this.options.wysiwyg.snippetsMenu && this.options.wysiwyg.snippetsMenu.$scrollable)\n                || (this.$scrollingElement.length && this.$scrollingElement)\n                || $().getScrollingElement(this.ownerDocument);\n            this.draggableComponent = new SmoothScrollOnDrag(this, this.$el, $scrollable, smoothScrollOptions);\n        } else {\n            this.$('.o_overlay_move_options').addClass('d-none');\n            $customize.find('.oe_snippet_clone').addClass('d-none');\n        }\n\n        if (!this.isTargetRemovable) {\n            this.$el.add($customize).find('.oe_snippet_remove').addClass('d-none');\n        }\n\n        var _animationsCount = 0;\n        var postAnimationCover = _.throttle(() => {\n            this.trigger_up('cover_update', {\n                overlayVisible: true,\n            });\n        }, 100);\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                    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', postAnimationCover);\n        // TODO this next part about editor replacement should be reviewed.\n        // When a change in the target requires a change of its editor, the\n        // two following events can be used:\n        // - before_replace_target: before the target gets replaced, makes sure\n        //   jquery data() do not reference the old editor anymore\n        // - replace_target: after the replacement happened in the DOM,\n        //   activates the replacement element.\n        this.$target.on('before_replace_target.snippet_editor', (ev, options) => {\n            ev.stopPropagation();\n            options.proms.push(new Promise(resolve => {\n                this.trigger_up('destroy_editor', {\n                    editor: this,\n                    onSuccess: resolve,\n                });\n            }));\n        });\n        this.$target.on('replace_target.snippet_editor', (ev, replacementEl) => {\n            this.trigger_up('activate_snippet', {$snippet: $(replacementEl)});\n        });\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        if (this.$optionsSection) {\n            this.$optionsSection.remove();\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    buildSnippet: async function () {\n        for (var i in this.styles) {\n            this.styles[i].onBuilt();\n        }\n        await this.toggleTargetVisibility(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        this.willDestroyEditors = true;\n        await this.toggleTargetVisibility(!this.$target.hasClass('o_snippet_invisible'));\n        const proms = _.map(this.styles, option => {\n            return option.cleanForSave();\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        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        var 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    },\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        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().tooltip('dispose');\n        this.$target.remove();\n        this.$el.remove();\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                const isEmpty = $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                return isEmpty && !$el.hasClass('oe_structure') && !$el.hasClass('oe_unremovable')\n                    && !$el.parent().hasClass('carousel-item')\n                    && (!editor || editor.isTargetParentEditable);\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            this.$el.find('.o_handle').removeClass('d-none');\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 = _.chain(editor.styles)\n                .values()\n                .sortBy('__order')\n                .value();\n\n            await focusOrBlur(editor, styles);\n        }\n        await Promise.all(editorUIsToUpdate.map(editor => editor.updateOptionsUI()));\n        await Promise.all(editorUIsToUpdate.map(editor => editor.updateOptionsUIVisibility()));\n\n        return this._customize$Elements;\n    },\n    /**\n     * @param {boolean} [show]\n     * @returns {Promise<boolean>}\n     */\n    toggleTargetVisibility: async function (show) {\n        show = this._toggleVisibilityStatus(show);\n        var styles = _.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.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     * @returns {Promise}\n     */\n    async updateOptionsUI() {\n        const proms = Object.values(this.styles).map(opt => {\n            return opt.updateUI({noVisibility: true});\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        return Promise.all(proms);\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        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\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        var $optionsSection = $(core.qweb.render('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        // TODO In master: restrict selectors to `:not(.o_disabled)`.\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 = _.map(this.templateOptions, val => {\n            if (!val.selector.is(this.$target)) {\n                return;\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\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                _.extend({\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            }\n\n            if (option.displayOverlayOptions) {\n                this.displayOverlayOptions = true;\n            }\n\n            return option.appendTo(document.createDocumentFragment());\n        });\n\n        this.isTargetMovable = (this.selectorSiblings.length > 0 || this.selectorChildren.length > 0);\n\n        this.$el.find('[data-toggle=\"dropdown\"]').dropdown();\n\n        return Promise.all(defs).then(() => {\n            const options = _.sortBy(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     * @private\n     * @param {boolean} [show]\n     */\n    _toggleVisibilityStatus: function (show) {\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 (snippetTemplate) {\n                    const forbidSanitize = snippetTemplate.dataset.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    //--------------------------------------------------------------------------\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: function () {\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.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 closestFormEl = this.$target[0].closest('form');\n        self.$target.after('<div class=\"oe_drop_clone\" style=\"display: none;\"/>');\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            if (!$selectorSiblings) {\n                $selectorSiblings = self.selectorSiblings[i].all();\n            } else {\n                $selectorSiblings = $selectorSiblings.add(self.selectorSiblings[i].all());\n            }\n        }\n        var $selectorChildren;\n        for (i = 0; i < self.selectorChildren.length; i++) {\n            if (!$selectorChildren) {\n                $selectorChildren = self.selectorChildren[i].all();\n            } else {\n                $selectorChildren = $selectorChildren.add(self.selectorChildren[i].all());\n            }\n        }\n        // TODO In master, do not reference other module class + find a better\n        // system to define such cases + avoid duplicated code (drag & drop from\n        // editor panel + drag & drop from move button of existing block).\n        // Prevent dropping ToC inside another ToC. grep: NO_DOUBLE_TOC\n        if (this.$target[0].classList.contains('s_table_of_content')) {\n            $selectorChildren = $selectorChildren.filter((i, el) => !el.closest('.s_table_of_content'));\n        }\n        // Disallow dropping form fields outside of their form.\n        // TODO this can probably be implemented by reviewing data-drop-near\n        // definitions in master but we should find a better to define those and\n        // such cases.\n        if (this.$target[0].matches('.form-group')) {\n            $selectorSiblings = $selectorSiblings.filter(\n                (i, el) => closestFormEl === el.closest('form')\n            );\n        }\n\n        const canBeSanitizedUnless = this._canBeSanitizedUnless(this.$target[0]);\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        });\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        this.$dropZones.droppable({\n            over: function () {\n                if (self.dropped) {\n                    self.$target.detach();\n                    $('.oe_drop_zone').removeClass('invisible');\n                }\n                self.dropped = true;\n                $(this).first().after(self.$target).addClass('invisible');\n            },\n            out: function () {\n                var prev = self.$target.prev();\n                if (this === prev[0]) {\n                    self.dropped = false;\n                    self.$target.detach();\n                    $(this).removeClass('invisible');\n                }\n            },\n        });\n\n        // Trigger a scroll on the draggable element so that jQuery updates\n        // the position of the drop zones.\n        self.draggableComponent.$scrollTarget.on('scroll.scrolling_element', function () {\n            self.$el.trigger('scroll');\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: function (ev, ui) {\n        this.options.wysiwyg.odooEditor.automaticStepActive();\n        this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n        this.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n\n        // TODO lot of this is duplicated code of the d&d feature of snippets\n        if (!this.dropped) {\n            var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first();\n            // Some drop zones might have been disabled.\n            $el = $el.filter(this.$dropZones);\n            if ($el.length) {\n                $el.after(this.$target);\n                this.dropped = true;\n            }\n        }\n\n        this.$dropZones.droppable('destroy');\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            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        this.draggableComponent.$scrollTarget.off('scroll.scrolling_element');\n        const samePositionAsStart = 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        delete this.$dropZones;\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            _.each(ev.data.optionNames, function (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.willDestroyEditors) {\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\n/**\n * Management of drag&drop menu and snippet related behaviors in the page.\n */\nvar SnippetsMenu = Widget.extend({\n    id: 'oe_snippets',\n    cacheSnippetTemplate: {},\n    events: {\n        'click .oe_snippet': '_onSnippetClick',\n        'click .o_install_btn': '_onInstallBtnClick',\n        'click .o_we_add_snippet_btn': '_onBlocksTabClick',\n        'click .o_we_customize_snippet_btn': '_onOptionsTabClick',\n        'click .o_we_invisible_entry': '_onInvisibleEntryClick',\n        'click #snippet_custom .o_rename_btn': '_onRenameBtnClick',\n        'click #snippet_custom .o_delete_btn': '_onDeleteBtnClick',\n        'mousedown': '_onMouseDown',\n        'input .o_snippet_search_filter_input': '_onSnippetSearchInput',\n        'click .o_snippet_search_filter_reset': '_onSnippetSearchResetClick',\n        'click .o_we_website_top_actions button[data-action=save]': '_onSaveRequest',\n        'click .o_we_website_top_actions button[data-action=cancel]': '_onDiscardClick',\n        'click .o_we_website_top_actions button[data-action=mobile]': '_onMobilePreviewClick',\n        'click .o_we_website_top_actions button[data-action=undo]': '_onUndo',\n        'click .o_we_website_top_actions button[data-action=redo]': '_onRedo',\n    },\n    custom_events: {\n        'activate_insertion_zones': '_onActivateInsertionZones',\n        'activate_snippet': '_onActivateSnippet',\n        'call_for_each_child_snippet': '_onCallForEachChildSnippet',\n        'clone_snippet': '_onCloneSnippet',\n        'cover_update': '_onOverlaysCoverUpdate',\n        'deactivate_snippet': '_onDeactivateSnippet',\n        'destroy_editor': '_onDestroyEditor',\n        'drag_and_drop_stop': '_onSnippetDragAndDropStop',\n        'drag_and_drop_start': '_onSnippetDragAndDropStart',\n        'get_snippet_versions': '_onGetSnippetVersions',\n        'find_snippet_template': '_onFindSnippetTemplate',\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        'reload_snippet_dropzones': '_disableUndroppableSnippets',\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    },\n    // enum of the SnippetsMenu's tabs.\n    tabs: {\n        BLOCKS: 'blocks',\n        OPTIONS: 'options',\n        CUSTOM: 'custom',\n    },\n\n    /**\n     * @param {Widget} parent\n     * @param {Object} [options]\n     * @param {string} [options.snippets]\n     *      URL of the snippets template. This URL might have been set\n     *      in the global 'snippets' variable, otherwise this function\n     *      assigns a default one.\n     *      default: 'web_editor.snippets'\n     *\n     * @constructor\n     */\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        options = options || {};\n        this.$body = $((options.document || document).body);\n\n        this.options = options;\n        if (!this.options.snippets) {\n            this.options.snippets = 'web_editor.snippets';\n        }\n        this.snippetEditors = [];\n        this._enabledEditorHierarchy = [];\n\n        this._mutex = new concurrency.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 .close',\n            '.o_we_crop_widget',\n            '.transfo-container',\n        ].join(', ');\n\n        this.loadingTimers = {};\n        this.loadingElements = {};\n        this._loadingEffectDisabled = false;\n    },\n    /**\n     * @override\n     */\n    willStart: function () {\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        ColorPaletteWidget.loadDependencies(this);\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async start() {\n        var defs = [this._super.apply(this, arguments)];\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\n        this.customizePanel = document.createElement('div');\n        this.customizePanel.classList.add('o_we_customize_panel', 'd-none');\n        this._addToolbar();\n        this._checkEditorToolbarVisibilityCallback = this._checkEditorToolbarVisibility.bind(this);\n        $(this.options.wysiwyg.odooEditor.document.body).on('click', this._checkEditorToolbarVisibilityCallback);\n        this.invisibleDOMPanelEl = document.createElement('div');\n        this.invisibleDOMPanelEl.classList.add('o_we_invisible_el_panel');\n        this.invisibleDOMPanelEl.appendChild(\n            $('<div/>', {\n                text: _t('Invisible Elements'),\n                class: 'o_panel_header',\n            })[0]\n        );\n\n        // Prepare snippets editor environment\n        this.$snippetEditorArea = $('<div/>', {\n            id: 'oe_manipulators',\n        }).insertAfter(this.$el);\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.$el.find('.o_we_website_top_actions').removeClass('d-none');\n            this.$('.o_snippet_search_filter').addClass('d-none');\n            this.$('#o_scroll').addClass('d-none');\n            this.$('button[data-action=\"mobilePreview\"]').addClass('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.customizePanel).removeClass('d-none');\n            this.$('#o_we_editor_toolbar_container').hide();\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        this.options.getScrollOptions = this._getScrollOptions.bind(this);\n\n        // Fetch snippet templates and compute it\n        defs.push((async () => {\n            await this._loadSnippetsTemplates();\n            await this._updateInvisibleDOM();\n        })());\n\n        // Active snippet editor on click in the page\n        var lastElement;\n        const onClick = ev => {\n            var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;\n            if (!srcElement || 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            lastElement = srcElement;\n            _.defer(function () {\n                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) {\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.$snippets.odooBounce();\n                });\n                return;\n            }\n            this._activateSnippet($target);\n        };\n\n        this.$document.on('click.snippets_menu', '*', onClick);\n        // Needed as bootstrap stop the propagation of click events for dropdowns\n        this.$document.on('mouseup.snippets_menu', '.dropdown-toggle', onClick);\n\n        core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);\n\n        // Adapt overlay covering when the window is resized / content changes\n        var debouncedCoverUpdate = _.throttle(() => {\n            this.updateCurrentSnippetEditorOverlay();\n        }, 50);\n        this.$window.on('resize.snippets_menu', debouncedCoverUpdate);\n        this.$window.on('content_changed.snippets_menu', debouncedCoverUpdate);\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.$document.on('keydown.snippets_menu', () => {\n            this.__overlayKeyWasDown = true;\n            this.snippetEditors.forEach(editor => {\n                editor.toggleOverlayVisibility(false);\n            });\n        });\n        this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', _.throttle(() => {\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        }, 250));\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.ownerDocument);\n        if (!this.$scrollingElement[0]) {\n            this.$scrollingElement = $(this.ownerDocument).find('.o_editable');\n        }\n        this.$scrollingTarget = this.$scrollingElement.is(this.ownerDocument.scrollingElement)\n            ? $(this.ownerDocument.defaultView)\n            : this.$scrollingElement;\n        this._onScrollingElementScroll = _.throttle(() => {\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        }, 50);\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        // Auto-selects text elements with a specific class and remove this\n        // on text changes\n        this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {\n            $(ev.target).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');\n            $(ev.target).selectContent();\n            $(ev.target).removeClass('o_default_snippet_text');\n        });\n        this.$document.on('keyup.snippets_menu', function () {\n            const selection = document.getSelection();\n            if (!Selection.rangeCount) {\n                return;\n            }\n            const range = selection.getRangeAt(0);\n            $(range.startContainer).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');\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.$document[0].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        // Add tooltips on we-title elements whose text overflows\n        this.$el.tooltip({\n            selector: 'we-title, [data-tooltip=\"true\"]',\n            placement: 'bottom',\n            delay: 100,\n            title: function () {\n                const el = this;\n                if (el.tagName !== 'WE-TITLE') {\n                    return el.title;\n                }\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        });\n\n        return Promise.all(defs).then(() => {\n            const $undoButton = this.$('.o_we_external_history_buttons button[data-action=\"undo\"]');\n            const $redoButton = this.$('.o_we_external_history_buttons button[data-action=\"redo\"]');\n            if ($undoButton.length) {\n                const updateHistoryButtons = () => {\n                    $undoButton.attr('disabled', !this.options.wysiwyg.odooEditor.historyCanUndo());\n                    $redoButton.attr('disabled', !this.options.wysiwyg.odooEditor.historyCanRedo());\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            }\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     * @override\n     */\n    destroy: function () {\n        this._super.apply(this, arguments);\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        core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);\n        $(document.body).off('click', this._checkEditorToolbarVisibilityCallback);\n        delete this.cacheSnippetTemplate[this.options.snippets];\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    cleanForSave: async function () {\n        // TODO remove me in master. This was added as a fix in stable to remove\n        // the \"data-snippet\" attribute that was added on the \"span\" element of\n        // the \"Cover\" snippet when modifying the \"Parallax\" of the snippet.\n        window.document.querySelectorAll(\"span[data-snippet='s_cover'][data-name='Cover']\")\n                    .forEach(el => {\n            delete el.dataset[\"snippet\"];\n            delete el.dataset[\"name\"];\n            const dirty = el.closest(\".o_editable\") || el;\n            dirty.classList.add(\"o_dirty\");\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.trigger_up('ready_to_clean_for_save');\n\n        this.willDestroyEditors = true;\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     * @param {boolean} invalidateCache\n     */\n    loadSnippets: function (invalidateCache) {\n        if (!invalidateCache && this.cacheSnippetTemplate[this.options.snippets]) {\n            this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets];\n            return this._defLoadSnippets;\n        }\n        this._defLoadSnippets = this._rpc({\n            model: 'ir.ui.view',\n            method: 'render_public_asset',\n            args: [this.options.snippets, {}],\n            kwargs: {\n                context: this.options.context,\n            },\n        });\n        this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;\n        return this._defLoadSnippets;\n    },\n    /**\n     * Get the editable area.\n     *\n     * @returns {JQuery}\n     */\n    getEditableArea: function () {\n        return this.options.wysiwyg.$editable.find(this.options.selectorEditableArea)\n            .add(this.options.wysiwyg.$editable.filter(this.options.selectorEditableArea));\n    },\n    /**\n     * Updates the cover dimensions of the current snippet editor.\n     */\n    updateCurrentSnippetEditorOverlay: function () {\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._currentTab === this.tabs.OPTIONS && !this.snippetEditors.length) {\n                this._activateEmptyOptionsTab();\n            }\n        });\n    },\n    activateCustomTab: function (content) {\n        this._updateRightPanelContent({content: content, tab: this.tabs.CUSTOM});\n    },\n    /**\n     * Public method to activate a snippet.\n     *\n     * @see this._activateSnippet\n     * @param {jQuery} $snippet\n     * @returns {Promise}\n     */\n    activateSnippet: async function ($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     * @returns {Promise}\n     */\n    callPostSnippetDrop: async function ($target) {\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();\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.trigger_up('snippet_dropped', {\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\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     */\n    _activateInsertionZones: function ($selectorSiblings, $selectorChildren) {\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        }\n\n        // Check if the drop zone should be horizontal or vertical\n        function setDropZoneDirection($elem, $parent, $sibling) {\n            var vertical = false;\n            var style = {};\n            $sibling = $sibling || $elem;\n            var css = window.getComputedStyle($elem[0]);\n            var parentCss = window.getComputedStyle($parent[0]);\n            var float = css.float || css.cssFloat;\n            var display = parentCss.display;\n            var flex = parentCss.flexDirection;\n            if (float === 'left' || float === 'right' || (display === 'flex' && flex === 'row')) {\n                style['float'] = float;\n                if ($sibling.parent().width() !== $sibling.outerWidth(true)) {\n                    vertical = true;\n                    style['height'] = Math.max($sibling.outerHeight(), 30) + 'px';\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\n        var $clone = $('.oe_drop_clone');\n        if ($clone.length) {\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());\n            } else {\n                data = {\n                    vertical: false,\n                    style: {},\n                };\n            }\n            self._insertDropzone($('<we-hook/>').insertAfter($clone), data.vertical, data.style);\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, $children.last());\n                    self._insertDropzone($('<we-hook/>').appendTo($zone), data.vertical, data.style);\n                }\n\n                if (!$zone.children().first().is('.oe_drop_clone')) {\n                    data = testPreviousSibling($zone[0].firstChild, $zone)\n                        || setDropZoneDirection($zone, $zone, $children.first());\n                    self._insertDropzone($('<we-hook/>').prependTo($zone), data.vertical, data.style);\n                }\n            });\n\n            // add children near drop zone\n            $selectorSiblings = $(_.uniq(($selectorSiblings || $()).add($selectorChildren.children()).get()));\n        }\n\n        var noDropZonesSelector = '[data-invisible=\"1\"], .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());\n                    self._insertDropzone($('<we-hook/>').insertBefore($zone), data.vertical, data.style);\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());\n                    self._insertDropzone($('<we-hook/>').insertAfter($zone), data.vertical, data.style);\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        $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    },\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: function () {\n        return this._execWithLoadingEffect(() => {\n            this.options.wysiwyg.odooEditor.automaticStepSkipStack();\n            this.invisibleDOMMap = new Map();\n            const $invisibleDOMPanelEl = $(this.invisibleDOMPanelEl);\n            $invisibleDOMPanelEl.find('.o_we_invisible_entry').remove();\n            const $selector = this.options.enableTranslation ? $('#wrapwrap') :\n                                                                globalSelector.all();\n            let $invisibleSnippets = $selector.find('.o_snippet_invisible')\n                                              .addBack('.o_snippet_invisible');\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            $invisibleDOMPanelEl.toggleClass('d-none', !$invisibleSnippets.length);\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(\".o_snippet_invisible\");\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,\n                                                  parentEl) => {\n                const editor = await this._createSnippetEditor($(invisibleSnippetEl));\n                const invisibleEntryEl = document.createElement(\"div\");\n                invisibleEntryEl.className = `${isRootParent ? \"o_we_invisible_root_parent\" : \"\"}`;\n                invisibleEntryEl.classList.add(\"o_we_invisible_entry\", \"d-flex\",\n                    \"align-items-center\", \"justify-content-between\");\n                invisibleEntryEl.innerText = isDescendant ? `\u2514 ${editor.getName()}` :\n                                                            editor.getName();\n                const iconEl = document.createElement(\"i\");\n                const eyeIconClass = editor.isTargetVisible() ? \"fa-eye\" : \"fa-eye-slash\";\n                iconEl.classList.add(\"fa\", \"ml-2\", eyeIconClass);\n                invisibleEntryEl.appendChild(iconEl);\n                parentEl.appendChild(invisibleEntryEl);\n                this.invisibleDOMMap.set(invisibleEntryEl, invisibleSnippetEl);\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 = async (snippetEls, isDescendant, parentEl) => {\n                for (const snippetEl of snippetEls) {\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                    await createInvisibleElement(snippetEl,\n                        !isDescendant && !!descendantSnippetEls, isDescendant, parentEl);\n                    if (descendantSnippetEls) {\n                        // Insert all the descendant snippets in a list.\n                        const listEntryEl = document.createElement(\"ul\");\n                        await createInvisibleElements(descendantSnippetEls, true, listEntryEl);\n                        parentEl.appendChild(listEntryEl);\n                    }\n                }\n            };\n            return createInvisibleElements(rootInvisibleSnippetEls, false, $invisibleDOMPanelEl[0]);\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    _activateSnippet: async function ($snippet, previewMode, ifInactiveOptions) {\n        if (this.options.enableTranslation) {\n            // In translate mode, do not activate the snippet when enabling its\n            // corresponding invisible element. Indeed, in translate mode, we\n            // only want to toggle its visibility.\n            return;\n        }\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        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: this.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                    this._updateRightPanelContent({\n                        content: customize$Elements || [],\n                        tab: customize$Elements ? this.tabs.OPTIONS : this.tabs.BLOCKS,\n                    });\n                }\n\n                return editorToEnable;\n            }).then(async editor => {\n                // If a link was clicked, the linktools should be focused after\n                // the right panel is shown to the user.\n                // TODO: this should be reviewed to be done another way: we\n                // should avoid focusing something here while it is being\n                // rendered elsewhere.\n                const linkTools = this.options.wysiwyg.linkTools;\n                if (linkTools && this._currentTab === this.tabs.OPTIONS\n                        && !linkTools.noFocusUrl) {\n                    // Wait for `linkTools` potential in-progress rendering\n                    // before focusing the URL input on `snippetsMenu` (this\n                    // prevents race condition for automated testing).\n                    await linkTools.renderingPromise;\n                    linkTools.focusUrl();\n                }\n                return editor;\n            });\n        });\n    },\n    /**\n     * @private\n     * @param {boolean} invalidateCache\n     */\n    _loadSnippetsTemplates: async function (invalidateCache) {\n        return this._execWithLoadingEffect(async () => {\n            await this._destroyEditors();\n            const html = await this.loadSnippets(invalidateCache);\n            await this._computeSnippetTemplates(html);\n        }, false);\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    _destroyEditors: async function ($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: function ($snippet, callback) {\n        var self = this;\n        var defs = _.map($snippet.add(globalSelector.all($snippet)), function (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: function () {\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     */\n    _computeSelectorFunctions: function (selector, exclude, target, noCheck, isChildren, excludeParent) {\n        var self = this;\n\n        // TODO in master: FOR_DROP should be a param of the function.\n        const forDropID = 'FOR_DROP';\n        const forDrop = exclude && exclude.startsWith(forDropID);\n        if (forDrop) {\n            exclude = exclude.substring(forDropID.length);\n        }\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        exclude += `${exclude && ', '}.o_snippet_not_selectable`;\n\n        let filterFunc = function () {\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 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).is('.o_not_editable :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        var functions = {\n            is: function ($from) {\n                return $from.is(selector) && $from.filter(filterFunc).length !== 0;\n            },\n        };\n        if (noCheck) {\n            functions.closest = function ($from, parentNode) {\n                return $from.closest(selector, parentNode).filter(filterFunc);\n            };\n            functions.all = function ($from) {\n                return ($from ? dom.cssFind($from, selector) : $(selector)).filter(filterFunc);\n            };\n        } else {\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 dom.cssFind($from || self.getEditableArea(), selector).filter(filterFunc);\n            } : function ($from) {\n                $from = $from || self.getEditableArea();\n                return $from.filter(selector).add(dom.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 {string} html\n     */\n    _computeSnippetTemplates: function (html) {\n        var self = this;\n        var $html = $(html);\n        var $scroll = $html.siblings('#o_scroll');\n\n        // TODO remove me in master: this is a hack that moves the logo options\n        // (type, height, height scrolled) into the navbar section. Before that\n        // these options were not displayed if the logo type was \"text\".\n        const $logoTypeSelector = $html.find('[data-js=\"HeaderNavbar\"] [data-name=\"option_header_brand_none\"]').parent();\n        if ($logoTypeSelector.length) {\n            $logoTypeSelector[0].dataset.dependencies = \"\";\n\n            const $logoHeightOptions = $html.find('[data-customize-website-variable][data-variable=\"logo-height\"], [data-customize-website-variable][data-variable=\"fixed-logo-height\"]');\n            $logoHeightOptions.closest('[data-selector]')\n                .find('[data-name=\"option_header_brand_none\"]').parent().remove();\n            for (const el of $logoHeightOptions) {\n                el.setAttribute('string', '\u2319 ' + el.getAttribute('string'));\n            }\n            $logoHeightOptions.insertAfter($logoTypeSelector);\n        }\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        const oldFooterSnippetsSelector = 'footer .oe_structure > *';\n        const newFooterSnippetsSelector = 'footer #footer.oe_structure > *:not(.s_popup)';\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\n            // TODO to remove in master: the Carousel snippet has a `content`\n            // class in its `.row` elements which makes dropzones appear when\n            // dragging inner content, allowing them to be dropped in the row,\n            // where it should not be the case.\n            if ($style[0].getAttribute('id') === 'so_content_addition') {\n                let dropInPatch = $style[0].dataset.dropIn.split(', ');\n                dropInPatch = dropInPatch.map(selector => selector === '.content' ? '.content:not(.row)' : selector);\n                $style[0].dataset.dropIn = dropInPatch.join(', ');\n            }\n\n            // Fix in stable: we have removed the option for setting the\n            // background color for snippets in the footer. However, this should\n            // not affect the snippets in the \"All pages\" popup which is also\n            // located in the footer.\n            if (($style[0].dataset.js === 'ColoredLevelBackground') && exclude) {\n                exclude = exclude\n                    .split(', ')\n                    .map(selector => selector === oldFooterSnippetsSelector ? newFooterSnippetsSelector : selector)\n                    .join(', ');\n            }\n            if (($style[0].dataset.js === 'BackgroundToggler')) {\n                selector = selector\n                    .split(', ')\n                    .map(selector => selector === oldFooterSnippetsSelector ? newFooterSnippetsSelector : selector)\n                    .join(', ');\n            }\n\n            var target = $style.data('target');\n            var noCheck = $style.data('no-check');\n            var optionID = $style.data('js') || $style.data('option-name'); // used in tour js as selector\n            // TODO: adapt in master - used to hide XML 'img' options when image\n            // is not supported.\n            const xmlImageOption = !$style[0].hasAttribute('data-js') && (selector.indexOf('img') !== -1);\n            const nonSupportedImageSelector = '[data-oe-type=\"image\"] > img, [data-oe-xpath]';\n            if (xmlImageOption && !noCheck) {\n                exclude = [exclude, nonSupportedImageSelector].filter(value => !!value).join(', ');\n            } else if (['ReplaceMedia', 'FontawesomeTools', 'WebsiteAnimate'].includes(optionID)) {\n                // TODO adapt in master: editable root elements are technically\n                // *potentially* supported (if the edited attributes are not\n                // computed inside the related view, they could technically be\n                // saved... but as we cannot tell the computed ones apart from\n                // the \"static\" ones, we choose to not support edition at all in\n                // those \"root\" cases). Here we explicitely exclude some options\n                // but we could exclude them all or do something smarter in\n                // future versions.\n                exclude = [exclude, '[data-oe-xpath]'].filter(value => !!value).join(', ');\n            }\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),\n                '$el': $style,\n                'drop-near': $style.data('drop-near') && self._computeSelectorFunctions($style.data('drop-near'), 'FOR_DROP', false, noCheck, true, excludeParent),\n                'drop-in': $style.data('drop-in') && self._computeSelectorFunctions($style.data('drop-in'), 'FOR_DROP', false, noCheck),\n                'data': _.extend({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) {\n            for (var i = 0, len = selectors.length; i < len; i++) {\n                if (selectors[i].is($from)) {\n                    return true;\n                }\n            }\n            return false;\n        };\n\n        this.$snippets = $scroll.find('.o_panel_body').children()\n            .addClass('oe_snippet')\n            .each((i, el) => {\n                const $snippet = $(el);\n                const name = _.escape(el.getAttribute('name'));\n                const thumbnailSrc = _.escape(el.dataset.oeThumbnail);\n                const $sbody = $snippet.children().addClass('oe_snippet_body');\n                const isCustomSnippet = !!el.closest('#snippet_custom');\n\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 = $sbody.attr('class').match(/s_[^ ]+/g);\n                if (snippetClasses && snippetClasses.length) {\n                    snippetClasses = '.' + snippetClasses.join('.');\n                }\n                const $els = $(snippetClasses).not('[data-name]').add($sbody);\n                $els.attr('data-name', name).data('name', name);\n\n                // TODO Adapt in each stable version to target specific\n                // snippets, remove in master.\n                if (!$snippet.data('oeForbidSanitize')) {\n                    if ($snippet[0].querySelector('form')) {\n                        // In stable, make sure that existing FORM snippets cannot\n                        // be dropped in sanitized HTML model fields.\n                        $snippet[0].dataset.oeForbidSanitize = 'form';\n                    }\n                    if ($snippet[0].querySelector('.s_embed_code_embedded')) {\n                        // In stable, make sure that existing embed code snippets cannot\n                        // be dropped in sanitized HTML model fields.\n                        $snippet[0].dataset.oeForbidSanitize = 'true';\n                    }\n                }\n\n                // Create the thumbnail\n                const $thumbnail = $(`\n                    <div class=\"oe_snippet_thumbnail\">\n                        <div class=\"oe_snippet_thumbnail_img\" style=\"background-image: url(${thumbnailSrc});\"/>\n                        <span class=\"oe_snippet_thumbnail_title\">${name}</span>\n                    </div>\n                `);\n                $snippet.prepend($thumbnail);\n\n                // Create the install button (t-install feature) if necessary\n                const moduleID = $snippet.data('moduleId');\n                if (moduleID) {\n                    el.classList.add('o_snippet_install');\n                    $thumbnail.append($('<button/>', {\n                        class: 'btn btn-primary o_install_btn w-100',\n                        type: 'button',\n                        text: _t(\"Install\"),\n                    }));\n                }\n\n                // Create the rename and delete button for custom snippets\n                if (isCustomSnippet) {\n                    const btnRenameEl = document.createElement('we-button');\n                    btnRenameEl.dataset.snippetId = $snippet.data('oeSnippetId');\n                    btnRenameEl.classList.add('o_rename_btn', 'fa', 'fa-pencil', 'btn', 'o_we_hover_success');\n                    btnRenameEl.title = _.str.sprintf(_t(\"Rename %s\"), name);\n                    $snippet.append(btnRenameEl);\n                    const btnEl = document.createElement('we-button');\n                    btnEl.dataset.snippetId = $snippet.data('oeSnippetId');\n                    btnEl.classList.add('o_delete_btn', 'fa', 'fa-trash', 'btn', 'o_we_hover_danger');\n                    btnEl.title = _.str.sprintf(_t(\"Delete %s\"), name);\n                    $snippet.append(btnEl);\n                }\n            })\n            .not('[data-module-id]');\n\n        // Hide scroll if no snippets defined\n        if (!this.$snippets.length) {\n            this.$el.detach();\n        }\n\n        // Register the text nodes that needs to be auto-selected on click\n        this._registerDefaultTexts();\n\n        // Force non editable part to contentEditable=false\n        $html.find('.o_not_editable').attr('contentEditable', false);\n\n        // TODO remove me in 16.0: introduced in a 14.0 fix to allow users to\n        // switch between the different tabs of a custom tabs even in the\n        // editor mode. Before this fix, it was not possible because the\n        // elements of the tabs located on the website and the elements of the\n        // save tabs located on the editor had the same id's so the anchors\n        // were referring to the wrong elements.\n        for (const customTabPaneEl of $html.find('#snippet_custom_body .tab-pane')) {\n            customTabPaneEl.removeAttribute('id');\n        }\n\n        // Add the computed template and make elements draggable\n        this.$el.html($html);\n        this.$el.append(this.customizePanel);\n        this.$el.append(this.invisibleDOMPanelEl);\n        this._makeSnippetDraggable(this.$snippets);\n        this._disableUndroppableSnippets();\n\n        this.$el.addClass('o_loaded');\n        this.trigger_up('snippets_loaded', self.$el);\n        $(this.el.ownerDocument.body).addClass('editor_has_snippets');\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     * @returns {Promise<SnippetEditor>}\n     */\n    _createSnippetEditor: function ($snippet) {\n        var self = this;\n        var snippetEditor = $snippet.data('snippet-editor');\n        if (snippetEditor) {\n            return snippetEditor.__isStarted;\n        }\n\n        var def;\n        if (this._allowParentsEditors($snippet)) {\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(parentEditor || self, $snippet, self.templateOptions, $snippet.closest('[data-oe-type=\"html\"], .oe_structure').add(editableArea), self.options);\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     * 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: function () {\n        var self = this;\n        var cache = {};\n        this.$snippets.each(function () {\n            var $snippet = $(this);\n            var $snippetBody = $snippet.find('.oe_snippet_body');\n            const isSanitizeForbidden = $snippet.data('oeForbidSanitize');\n            const filterSanitize = isSanitizeForbidden === 'form'\n                ? $els => $els.filter((i, el) => !el.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"])'))\n                : isSanitizeForbidden\n                    ? $els => $els.filter((i, el) => !el.closest('[data-oe-sanitize]'))\n                    : $els => $els;\n\n            var check = false;\n            _.each(self.templateOptions, function (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'] ? filterSanitize(option['drop-near'].all()).length : 0,\n                    'drop-in': option['drop-in'] ? filterSanitize(option['drop-in'].all()).length : 0,\n                };\n                check = (cache[k]['drop-near'] || cache[k]['drop-in']);\n            });\n\n            $snippet.toggleClass('o_disabled', !check);\n            $snippet.attr('title', check ? '' : _t(\"No location to drop in\"));\n            const $icon = $snippet.find('.o_snippet_undroppable').remove();\n            if (check) {\n                $icon.remove();\n            } else if (!$icon.length) {\n                const imgEl = document.createElement('img');\n                imgEl.classList.add('o_snippet_undroppable');\n                imgEl.src = '/web_editor/static/src/img/snippet_disabled.svg';\n                $snippet.append(imgEl);\n            }\n        });\n    },\n    /**\n     * @private\n     * @param {string} [search]\n     */\n    _filterSnippets(search) {\n        const searchInputEl = this.el.querySelector('.o_snippet_search_filter_input');\n        const searchInputReset = this.el.querySelector('.o_snippet_search_filter_reset');\n        if (search !== undefined) {\n            searchInputEl.value = search;\n        } else {\n            search = searchInputEl.value;\n        }\n        search = search.toLowerCase();\n        searchInputReset.classList.toggle('d-none', !search);\n        const strMatches = str => !search || str.toLowerCase().includes(search);\n        for (const panelEl of this.el.querySelectorAll('.o_panel')) {\n            let hasVisibleSnippet = false;\n            const panelTitle = panelEl.querySelector('.o_panel_header').textContent;\n            const isPanelTitleMatch = strMatches(panelTitle);\n            for (const snippetEl of panelEl.querySelectorAll('.oe_snippet')) {\n                const matches = (isPanelTitleMatch\n                    || strMatches(snippetEl.getAttribute('name'))\n                    || strMatches(snippetEl.dataset.oeKeywords || ''));\n                if (matches) {\n                    hasVisibleSnippet = true;\n                }\n                snippetEl.classList.toggle('d-none', !matches);\n            }\n            panelEl.classList.toggle('d-none', !hasVisibleSnippet);\n        }\n    },\n    /**\n     * @private\n     * @param {Object} [options={}]\n     * @returns {Object}\n     */\n    _getScrollOptions(options = {}) {\n        return Object.assign({}, options, {\n            scrollBoundaries: Object.assign({\n                right: false,\n            }, options.scrollBoundaries),\n            jQueryDraggableOptions: Object.assign({\n                appendTo: this.$body,\n                cursor: 'move',\n                greedy: true,\n                scroll: false,\n            }, options.jQueryDraggableOptions),\n            disableHorizontalScroll: true,\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     */\n    _insertDropzone: function ($hook, vertical, style) {\n        // TODO Done this way in stable but should be a parameter in master.\n        let forbidSanitize;\n        if (this._insertDropzoneCanBeSanitizedUnless === 'form') {\n            forbidSanitize = $hook.closest('[data-oe-sanitize]:not([data-oe-sanitize=\"allow_form\"])').length;\n        } else {\n            forbidSanitize = !this._insertDropzoneCanBeSanitizedUnless && $hook.closest('[data-oe-sanitize]').length;\n        }\n        var $dropzone = $('<div/>', {\n            'class': '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     * Make given snippets be draggable/droppable thanks to their thumbnail.\n     *\n     * @private\n     * @param {jQuery} $snippets\n     */\n    _makeSnippetDraggable: function ($snippets) {\n        var self = this;\n        var $toInsert, dropped, $snippet;\n        let $dropZones;\n\n        let dragAndDropResolve;\n        let $scrollingElement = $().getScrollingElement(this.ownerDocument);\n        if (!$scrollingElement[0] || $scrollingElement.find('body.o_in_iframe').length) {\n            $scrollingElement = $(this.ownerDocument).find('.o_editable');\n        }\n\n        const smoothScrollOptions = this._getScrollOptions({\n            jQueryDraggableOptions: {\n                handle: '.oe_snippet_thumbnail:not(.o_we_already_dragging)',\n                cancel: '.oe_snippet.o_disabled',\n                helper: function () {\n                    const dragSnip = this.cloneNode(true);\n                    dragSnip.querySelectorAll('.o_delete_btn, .o_rename_btn').forEach(\n                        el => el.remove()\n                    );\n                    return dragSnip;\n                },\n                start: function () {\n                    const prom = new Promise(resolve => dragAndDropResolve = () => resolve());\n                    self._mutex.exec(() => prom);\n\n                    const doc = self.options.wysiwyg.odooEditor.document;\n                    $(doc.body).addClass('oe_dropzone_active');\n\n                    self.options.wysiwyg.odooEditor.automaticStepUnactive();\n\n                    self.$el.find('.oe_snippet_thumbnail').addClass('o_we_already_dragging');\n                    self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet');\n\n                    dropped = false;\n                    $snippet = $(this);\n                    var $baseBody = $snippet.find('.oe_snippet_body');\n                    var $selectorSiblings = $();\n                    var $selectorChildren = $();\n                    var temp = self.templateOptions;\n                    for (var 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                        }\n                    }\n\n                    // TODO mentioning external app snippet but done as a stable fix\n                    // that will be adapted in master: if popup snippet, do not\n                    // allow to add it in another snippet\n                    if ($baseBody[0].matches('.s_popup, .o_newsletter_popup')) {\n                        $selectorChildren = $selectorChildren.not('[data-snippet] *');\n                    }\n                    // TODO In master, do not reference other module class +\n                    // find a better system to define such cases + avoid\n                    // duplicated code (drag & drop from editor panel + drag &\n                    // drop from move button of existing block).\n                    // Prevent dropping ToC inside another ToC.\n                    // grep: NO_DOUBLE_TOC\n                    if ($baseBody[0].classList.contains('s_table_of_content')) {\n                        $selectorChildren = $selectorChildren.filter((i, el) => !el.closest('.s_table_of_content'));\n                    }\n\n                    $toInsert = $baseBody.clone();\n                    // Color-customize dynamic SVGs in dropped snippets with current theme colors.\n                    [...$toInsert.find('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, 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                    // TODO Done this way in stable but should be a parameter in\n                    // master.\n                    const forbidSanitize = $snippet.data('oeForbidSanitize');\n                    self._insertDropzoneCanBeSanitizedUnless = forbidSanitize === 'form' ? 'form' : !forbidSanitize;\n                    self._activateInsertionZones($selectorSiblings, $selectorChildren);\n                    delete self._insertDropzoneCanBeSanitizedUnless;\n                    $dropZones = self.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                    $dropZones.droppable({\n                        over: function () {\n                            if (dropped) {\n                                $toInsert.detach();\n                                $toInsert.addClass('oe_snippet_body');\n                                $('.oe_drop_zone').removeClass('invisible');\n                            }\n                            dropped = true;\n                            $(this).first().after($toInsert).addClass('invisible');\n                            $toInsert.removeClass('oe_snippet_body');\n                            self.trigger_up('drop_zone_over');\n                        },\n                        out: function () {\n                            var prev = $toInsert.prev();\n                            if (this === prev[0]) {\n                                dropped = false;\n                                $toInsert.detach();\n                                $(this).removeClass('invisible');\n                                $toInsert.addClass('oe_snippet_body');\n                            }\n                            self.trigger_up('drop_zone_out');\n                        },\n                    });\n                    // If a modal is open, the scroll target must be that modal\n                    const $openModal = self.getEditableArea().find('.modal:visible');\n                    if ($openModal.length) {\n                        self.draggableComponent.$scrollTarget = $openModal;\n                    }\n\n                    // Trigger a scroll on the draggable element so that jQuery updates\n                    // the position of the drop zones.\n                    self.draggableComponent.$scrollTarget.on('scroll.scrolling_element', function () {\n                        self.$el.trigger('scroll');\n                    });\n                    self.trigger_up('drop_zone_start');\n                },\n                stop: async function (ev, ui) {\n                    const doc = self.options.wysiwyg.odooEditor.document;\n                    $(doc.body).removeClass('oe_dropzone_active');\n                    self.options.wysiwyg.odooEditor.automaticStepActive();\n                    self.options.wysiwyg.odooEditor.automaticStepSkipStack();\n                    $toInsert.removeClass('oe_snippet_body');\n                    self.draggableComponent.$scrollTarget.off('scroll.scrolling_element');\n                    if (!dropped && ui.position.top > 3 && ui.position.left + ui.helper.outerHeight() < self.el.getBoundingClientRect().left) {\n                        const point = {x: ui.position.left, y: ui.position.top};\n                        const container = {container: doc.body};\n                        let droppedOnNotNearest = doc.defaultView.$.touching(\n                            point, '.oe_structure_not_nearest', container\n                        ).first();\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.length\n                            ? '.oe_drop_zone'\n                            : ':not(.oe_structure_not_nearest) > .oe_drop_zone';\n                        let $el = doc.defaultView.$.nearest(\n                            point, selector, container\n                        ).first();\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                    $dropZones.droppable('destroy');\n                    self.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                    self.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\n                        self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet');\n                        await self._scrollToSnippet($target, self.$scrollable);\n                        self.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet');\n\n\n                        _.defer(async function () {\n                            // Free the mutex now to allow following operations\n                            // (mutexed as well).\n                            dragAndDropResolve();\n\n                            await self.callPostSnippetDrop($target);\n\n                            // Restore editor to its normal edition state, also\n                            // make sure the undroppable snippets are updated.\n                            self._disableUndroppableSnippets();\n                            self.options.wysiwyg.odooEditor.unbreakableStepUnactive();\n                            self.options.wysiwyg.odooEditor.historyStep();\n                            self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging');\n                        });\n                    } else {\n                        $toInsert.remove();\n                        if (dragAndDropResolve) {\n                            dragAndDropResolve();\n                        }\n                        self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging');\n                    }\n                    self.trigger_up('drop_zone_stop');\n                },\n            },\n        });\n        this.draggableComponent = new SmoothScrollOnDrag(this, $snippets, $scrollingElement, smoothScrollOptions);\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: function ($in) {\n        if ($in === undefined) {\n            $in = this.$snippets.find('.oe_snippet_body');\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: function ({content, tab, ...options}) {\n        this._closeWidgets();\n\n        this._currentTab = tab || this.tabs.BLOCKS;\n\n        this._$toolbarContainer[0].remove();\n        if (content) {\n            while (this.customizePanel.firstChild) {\n                this.customizePanel.removeChild(this.customizePanel.firstChild);\n            }\n            $(this.customizePanel).append(content);\n            if (this._currentTab === this.tabs.OPTIONS && !options.forceEmptyTab) {\n                this._addToolbar();\n            }\n        }\n\n        this.$('.o_snippet_search_filter').toggleClass('d-none', this._currentTab !== this.tabs.BLOCKS);\n        this.$('#o_scroll').toggleClass('d-none', this._currentTab !== this.tabs.BLOCKS);\n        this.customizePanel.classList.toggle('d-none', this._currentTab === this.tabs.BLOCKS);\n        // Remove active class of custom button (e.g. mass mailing theme selection).\n        this.$('#snippets_menu button').removeClass('active');\n        this.$('.o_we_add_snippet_btn').toggleClass('active', this._currentTab === this.tabs.BLOCKS);\n        this.$('.o_we_customize_snippet_btn').toggleClass('active', this._currentTab === this.tabs.OPTIONS);\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 && !dom.hasScrollableContent(modalEl)) {\n            return;\n        }\n        return dom.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('i');\n        const loaderContainerClassList = [\n            'o_we_ui_loading',\n            'd-flex',\n            'justify-content-center',\n            'align-items-center',\n        ];\n        const loaderClassList = [\n            'fa',\n            'fa-circle-o-notch',\n            'fa-spin',\n            'fa-4x',\n        ];\n        loaderContainer.classList.add(...loaderContainerClassList);\n        loader.classList.add(...loaderClassList);\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} [contentLoading=true]\n     * @param {number} [delay=500]\n     * @returns {Promise}\n     */\n    async _execWithLoadingEffect(action, contentLoading = true, delay = 500) {\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     * @private\n     */\n    _allowParentsEditors($snippet) {\n        return !this.options.enableTranslation\n            && !$snippet[0].classList.contains(\"o_no_parent_editor\");\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\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: function (ev) {\n        // TODO Done this way in stable but should be a parameter in master.\n        this._insertDropzoneCanBeSanitizedUnless = ev.data.canBeSanitizedUnless;\n        this._activateInsertionZones(ev.data.$selectorSiblings, ev.data.$selectorChildren);\n        delete this._insertDropzoneCanBeSanitizedUnless;\n    },\n    /**\n     * Called when a child editor asks to deactivate the current snippet\n     * overlay.\n     *\n     * @private\n     */\n    _onActivateSnippet: function (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: function (ev) {\n        const prom = this._callForEachChildSnippet(ev.data.$snippet, ev.data.callback);\n        if (ev.data.onSuccess) {\n            prom.then(() => ev.data.onSuccess());\n        }\n    },\n    /**\n     * Called when the overlay dimensions/positions should be recomputed.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onOverlaysCoverUpdate: function (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    _onCloneSnippet: async function (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 deactivate the current snippet\n     * overlay.\n     *\n     * @private\n     */\n    _onDeactivateSnippet: function () {\n        this._activateSnippet(false);\n    },\n    /**\n     * Called when a child editor asks to be destroyed.\n     *\n     * @private\n     */\n    _onDestroyEditor: function (ev) {\n        function onSuccess() {\n            if (ev.data.onSuccess) {\n                ev.data.onSuccess();\n            }\n        }\n        this._mutex.exec(() => this._destroyEditor(ev.data.editor))\n            .then(onSuccess)\n            .guardedCatch(onSuccess);\n    },\n    /**\n    * Called when a snippet will move in the page.\n    *\n    * @private\n    */\n   _onSnippetDragAndDropStart: function () {\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    _onSnippetDragAndDropStop: async function (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     * Returns the droppable snippet from which a dropped snippet originates.\n     *\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onFindSnippetTemplate(ev) {\n        this.$snippets.each(function () {\n            const snippetBody = this.querySelector(`.oe_snippet_body[data-snippet=${ev.data.snippet.dataset.snippet}]`);\n            if (snippetBody) {\n                ev.data.callback(snippetBody.parentElement);\n                return false;\n            }\n        });\n    },\n    /**\n     * @private\n     */\n    _onHideOverlay: function () {\n        for (const editor of this.snippetEditors) {\n            editor.toggleOverlay(false);\n        }\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInstallBtnClick: function (ev) {\n        var self = this;\n        var $snippet = $(ev.currentTarget).closest('[data-module-id]');\n        var moduleID = $snippet.data('moduleId');\n        var name = $snippet.attr('name');\n        new Dialog(this, {\n            title: _.str.sprintf(_t(\"Install %s\"), name),\n            size: 'medium',\n            $content: $('<div/>', {text: _.str.sprintf(_t(\"Do you want to install the %s App?\"), name)}).append(\n                $('<a/>', {\n                    target: '_blank',\n                    href: '/web#id=' + encodeURIComponent(moduleID) + '&view_type=form&model=ir.module.module&action=base.open_module_tree',\n                    text: _t(\"More info about this app.\"),\n                    class: 'ml4',\n                })\n            ),\n            buttons: [{\n                text: _t(\"Save and Install\"),\n                classes: 'btn-primary',\n                click: function () {\n                    this.$footer.find('.btn').toggleClass('o_hidden');\n                    this._rpc({\n                        model: 'ir.module.module',\n                        method: 'button_immediate_install',\n                        args: [[moduleID]],\n                    }).then(() => {\n                        self.trigger_up('request_save', {\n                            reloadEditor: true,\n                            _toMutex: true,\n                        });\n                    }).guardedCatch(reason => {\n                        reason.event.preventDefault();\n                        this.close();\n                        const message = sprintf(Markup(_t(\"Could not install module <strong>%s</strong>\")), name);\n                        self.displayNotification({\n                            message: message,\n                            type: 'danger',\n                            sticky: true,\n                        });\n                    });\n                },\n            }, {\n                text: _t(\"Install in progress\"),\n                icon: 'fa-spin fa-circle-o-notch fa-spin mr8',\n                classes: 'btn-primary disabled o_hidden',\n            }, {\n                text: _t(\"Cancel\"),\n                close: true,\n            }],\n        }).open();\n    },\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInvisibleEntryClick: async function (ev) {\n        ev.preventDefault();\n        const $snippet = $(this.invisibleDOMMap.get(ev.currentTarget));\n        const isVisible = await this._execWithLoadingEffect(async () => {\n            const editor = await this._createSnippetEditor($snippet);\n            return editor.toggleTargetVisibility();\n        }, true);\n        $(ev.currentTarget).find('.fa')\n            .toggleClass('fa-eye', isVisible)\n            .toggleClass('fa-eye-slash', !isVisible);\n        return this._activateSnippet(isVisible ? $snippet : false);\n    },\n    /**\n     * @private\n     */\n    _onBlocksTabClick: function (ev) {\n        this._activateSnippet(false);\n    },\n    /**\n     * @private\n     */\n    _onOptionsTabClick: function (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: function (ev) {\n        const $snippet = $(ev.target).closest('.oe_snippet');\n        const snippetId = parseInt(ev.currentTarget.dataset.snippetId);\n        ev.stopPropagation();\n        new Dialog(this, {\n            size: 'medium',\n            title: _t('Confirmation'),\n            $content: $('<div><p>' + _.str.sprintf(_t(\"Are you sure you want to delete the snippet: %s ?\"), $snippet.attr('name')) + '</p></div>'),\n            buttons: [{\n                text: _t(\"Yes\"),\n                close: true,\n                classes: 'btn-primary',\n                click: async () => {\n                    await this._rpc({\n                        model: 'ir.ui.view',\n                        method: 'delete_snippet',\n                        kwargs: {\n                            'view_id': snippetId,\n                            'template_key': this.options.snippets,\n                        },\n                    });\n                    await this._loadSnippetsTemplates(true);\n                },\n            }, {\n                text: _t(\"No\"),\n                close: true,\n            }],\n        }).open();\n    },\n    /**\n     * @private\n     */\n    _onRenameBtnClick: function (ev) {\n        const $snippet = $(ev.target).closest('.oe_snippet');\n        const snippetName = $snippet.attr('name');\n        const confirmText = _t('Confirm');\n        const cancelText = _t('Cancel');\n        const $input = $(`\n            <we-input class=\"o_we_user_value_widget w-100 mx-1\">\n                <div>\n                    <input type=\"text\" autocomplete=\"chrome-off\" value=\"${snippetName}\" class=\"text-left\"/>\n                    <we-button class=\"o_we_confirm_btn o_we_text_success fa fa-check\" title=\"${confirmText}\"/>\n                    <we-button class=\"o_we_cancel_btn o_we_text_danger fa fa-times\" title=\"${cancelText}\"/>\n                </div>\n            </we-input>\n        `);\n        $snippet.find('we-button').remove();\n        $snippet.find('span.oe_snippet_thumbnail_title').replaceWith($input);\n        const $textInput = $input.find('input');\n        $textInput.focus();\n        $textInput.select();\n        $snippet.find('.oe_snippet_thumbnail').addClass('o_we_already_dragging'); // prevent drag\n        $input.find('.o_we_confirm_btn').click(async () => {\n            const name = $textInput.val();\n            if (name !== snippetName) {\n                this._execWithLoadingEffect(async () => {\n                    await this._rpc({\n                        model: 'ir.ui.view',\n                        method: 'rename_snippet',\n                        kwargs: {\n                            'name': name,\n                            'view_id': parseInt(ev.target.dataset.snippetId),\n                            'template_key': this.options.snippets,\n                        },\n                    });\n                }, true);\n            }\n            await this._loadSnippetsTemplates(name !== snippetName);\n        });\n        $input.find('.o_we_cancel_btn').click(async () => {\n            await this._loadSnippetsTemplates(false);\n        });\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: function () {\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     * @param {OdooEvent} ev\n     */\n    _onGetSnippetVersions: function (ev) {\n        const snippet = this.el.querySelector(`.oe_snippet > [data-snippet=\"${ev.data.snippetName}\"]`);\n        ev.data.onSuccess(snippet && {\n            vcss: snippet.dataset.vcss,\n            vjs: snippet.dataset.vjs,\n            vxml: snippet.dataset.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    _onReloadSnippetTemplate: async function (ev) {\n        await this._activateSnippet(false);\n        await this._loadSnippetsTemplates(true);\n    },\n    /**\n     * @private\n     */\n    _onBlockPreviewOverlays: function (ev) {\n        this._blockPreviewOverlays = true;\n    },\n    /**\n     * @private\n     */\n    _onUnblockPreviewOverlays: function (ev) {\n        this._blockPreviewOverlays = false;\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onRemoveSnippet: async function (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: function (ev) {\n        const data = ev.data || {};\n        if (ev.target === this && !data._toMutex) {\n            return;\n        }\n        delete data._toMutex;\n        ev.stopPropagation();\n        this._execWithLoadingEffect(() => {\n            if (data.reloadEditor) {\n                data.reload = false;\n                const oldOnSuccess = data.onSuccess;\n                data.onSuccess = async function () {\n                    if (oldOnSuccess) {\n                        await oldOnSuccess.call(this, ...arguments);\n                    }\n                    window.location.href = window.location.origin + window.location.pathname + '?enable_editor=1';\n                };\n            }\n            this.trigger_up('request_save', data);\n        }, true);\n    },\n    /**\n     * @private\n     */\n    _onSnippetClick() {\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     * @private\n     * @param {OdooEvent} ev\n     * @param {Object} ev.data\n     * @param {function} ev.data.exec\n     */\n    _onSnippetEditionRequest: function (ev) {\n        this._execWithLoadingEffect(ev.data.exec, 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: function (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: function () {\n        this._disableUndroppableSnippets();\n        this._updateInvisibleDOM();\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     * @param {OdooEvent} ev\n     */\n    _onSnippetOptionUpdate(ev) {\n        ev.stopPropagation();\n        (async () => {\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            ev.data.onSuccess();\n        })();\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onSnippetOptionVisibilityUpdate: async function (ev) {\n        if (this.willDestroyEditors) {\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        const $snippet = this.$snippets.has(`[data-snippet=\"${ev.data.key}\"]`);\n        ev.data.onSuccess($snippet.length ? $snippet[0].dataset.oeThumbnail : '');\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: function () {\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: function () {\n        this.el.classList.remove('o_we_backdrop');\n    },\n    /**\n     * Called when search input value changed -> adapts the snippets grid.\n     *\n     * @private\n     */\n    _onSnippetSearchInput: function () {\n        this._filterSnippets();\n    },\n    /**\n     * Called on snippet search filter reset -> clear input field search.\n     *\n     * @private\n     */\n    _onSnippetSearchResetClick: function () {\n        this._filterSnippets('');\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        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\n        this.options.wysiwyg.toolbar.el.classList.remove('oe-floating');\n\n        // Create toolbar custom container.\n        this._$toolbarContainer = $('<WE-CUSTOMIZEBLOCK-OPTIONS id=\"o_we_editor_toolbar_container\"/>');\n        const $title = $(\"<we-title><span>\" + titleText + \"</span></we-title>\");\n\n        this._$toolbarContainer.append($title);\n        this._$toolbarContainer.append(this.options.wysiwyg.toolbar.$el);\n        $(this.customizePanel).append(this._$toolbarContainer);\n\n        // Create table-options custom container.\n        const $customizeTableBlock = $(QWeb.render('web_editor.toolbar.table-options'));\n        this.options.wysiwyg.odooEditor.bindExecCommand($customizeTableBlock[0]);\n\n        $(this.customizePanel).append($customizeTableBlock);\n\n        this._$removeFormatButton = this._$removeFormatButton || this.options.wysiwyg.toolbar.$el.find('#removeFormat');\n        $title.append(this._$removeFormatButton);\n        this.options.wysiwyg.toolbar.$el.find('#table').remove();\n\n        this._checkEditorToolbarVisibility();\n    },\n    /**\n     * Update editor UI visibility based on the current range.\n     */\n    _checkEditorToolbarVisibility: function (e) {\n        const $toolbarContainer = this.$('#o_we_editor_toolbar_container');\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        if (!range ||\n            !$currentSelectionTarget.parents('#wrapwrap, .iframe-editor-wrapper .o_editable').length ||\n            $(selection.anchorNode).parent('[data-oe-model]:not([data-oe-type=\"html\"]):not([data-oe-field=\"arch\"]):not([data-oe-translation-id])').length ||\n            $(selection.focusNode).parent('[data-oe-model]:not([data-oe-type=\"html\"]):not([data-oe-field=\"arch\"]):not([data-oe-translation-id])').length ||\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            $toolbarContainer.hide();\n        } else {\n            $toolbarContainer.show();\n        }\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: function () {\n        this.trigger_up('request_cancel');\n    },\n    /**\n     * Preview on mobile.\n     */\n    _onMobilePreviewClick: async function () {\n        throw new Error('implement me');\n    },\n    /**\n     * Undo..\n     */\n    _onUndo: async function () {\n        this.options.wysiwyg.undo();\n    },\n    /**\n     * Redo.\n     */\n    _onRedo: async function () {\n        this.options.wysiwyg.redo();\n    },\n    /**\n     * @private\n     * @param {OdooEvent} ev\n     */\n    _onRequestEditable: function (ev) {\n        ev.data.callback($(this.options.wysiwyg.odooEditor.editable));\n    },\n    /**\n     * Enable loading effects\n     *\n     * @private\n     */\n    _onEnableLoadingEffect: function () {\n        this._loadingEffectDisabled = false;\n    },\n    /**\n     * Disable loading effects and cancel the one displayed\n     *\n     * @private\n     */\n    _onDisableLoadingEffect: function () {\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\nreturn {\n    SnippetsMenu: SnippetsMenu,\n    SnippetEditor: SnippetEditor,\n    globalSelector: globalSelector,\n};\n});\n", "odoo.define('web_editor.toolbar', function (require) {\n'use strict';\n\nvar Widget = require('web.Widget');\n\nconst Toolbar = Widget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],\n\n    /**\n     * @constructor\n     * @param {Widget} parent\n     * @param {string} contents\n     */\n    init: function (parent, template = 'web_editor.toolbar') {\n        this._super.apply(this, arguments);\n        this.template = template;\n    },\n});\n\nreturn Toolbar;\n\n});\n", "odoo.define('web_editor.snippets.options', function (require) {\n'use strict';\n\nvar core = require('web.core');\nconst {ColorpickerWidget} = require('web.Colorpicker');\nconst Dialog = require('web.Dialog');\nconst {scrollTo} = require('web.dom');\nconst rpc = require('web.rpc');\nconst time = require('web.time');\nconst utils = require('web.utils');\nvar Widget = require('web.Widget');\nvar ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget;\nconst weUtils = require('web_editor.utils');\nconst {\n    normalizeColor,\n    getBgImageURL,\n    backgroundImageCssToParts,\n    backgroundImagePartsToCss,\n    DEFAULT_PALETTE,\n    isBackgroundImageAttribute,\n} = weUtils;\nvar weWidgets = require('wysiwyg.widgets');\nconst {\n    loadImage,\n    loadImageInfo,\n    applyModifications,\n    removeOnImageChangeAttrs,\n    isImageSupportedForProcessing,\n    isImageSupportedForStyle,\n} = require('web_editor.image_processing');\nconst OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor');\n\nvar qweb = core.qweb;\nvar _t = core._t;\nconst preserveCursor = OdooEditorLib.preserveCursor;\nconst descendants = OdooEditorLib.descendants;\n\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    }\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    // As a stable fix, to not touch XML templates and break existing\n    // translations, the \u2319 character is automatically replaced by \u2514 which makes\n    // more sense for the usecase and should work properly in all browsers. The\n    // \u2319 character is actually rendered mirrored on Windows 11 Chrome (and\n    // others) as the font used for those unicode characters is left to the\n    // browser. We could force a font of our own but it's probably not worth it.\n    // TODO a better solution with a SVG or CSS solution has to be done in\n    // master. That would unify the look of the symbol across all browsers and\n    // also prevent special characters to be placed in translations.\n    titleEl.textContent = title.replace(/\u2319/g, '\u2514');\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            if (src.split('.').pop() === 'svg') {\n                const response = await window.fetch(src);\n                const text = await response.text();\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        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    if (_t.database.parameters.direction === 'rtl') {\n        togglerEl.classList.add('o_we_collapse_toggler_rtl');\n    }\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 = 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.imgEl = await _buildImgElement(this.options.dataAttributes.img);\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.imgEl) {\n            this.containerEl.appendChild(this.imgEl);\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 = _.extend({}, 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 = _.extend({}, 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 = _.extend({}, 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.imgEl) {\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.imgEl && this.activeImgEl) {\n            this.imgEl.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 => node && this.menuEl.appendChild(node));\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\n    /**\n     * @override\n     */\n    async start() {\n        await this._super(...arguments);\n        if (!this.menuEl.children.length) {\n            // Remove empty text nodes so that :empty css rule can work\n            // TODO this has been added here as a fix to be extra careful. In\n            // master we should just avoid adding text nodes inside\n            // we-selection-items in the first place.\n            while (this.menuEl.firstChild\n                    && !this.menuEl.firstChild.data.trim().length) {\n                this.menuEl.firstChild.remove();\n            }\n        }\n\n        if (this.options && this.options.valueEl) {\n            this.containerEl.insertBefore(this.options.valueEl, this.menuEl);\n        }\n\n        this.menuTogglerEl = document.createElement('we-toggler');\n        this.iconEl = this.imgEl || 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        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    },\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            if (value) {\n                textContent = value;\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 = \"/\";\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    //--------------------------------------------------------------------------\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 (!params.unit) {\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 (!params.unit) {\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        const params = this._methodsParams;\n        if (!params.unit) {\n            return isSuperActive;\n        }\n        return isSuperActive && this._floatToStr(parseFloat(this._value)) !== '0';\n    },\n    /**\n     * @override\n     */\n    async setValue(value, methodName) {\n        const params = this._methodsParams;\n        if (params.unit) {\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\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        this.inputEl.classList.toggle('text-left', !unit);\n        this.inputEl.classList.toggle('text-right', !!unit);\n        this.containerEl.appendChild(this.inputEl);\n\n        var unitEl = document.createElement('span');\n        unitEl.textContent = unit;\n        this.containerEl.appendChild(unitEl);\n        if (unit.length > 3) {\n            this.el.classList.add('o_we_large');\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    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onInputInput: function (ev) {\n        this._value = this.inputEl.value;\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 (!params.unit && !params.step) {\n            return;\n        }\n        switch (ev.which) {\n            case $.ui.keyCode.ENTER:\n                this._onUserValueChange(ev);\n                break;\n            case $.ui.keyCode.UP:\n            case $.ui.keyCode.DOWN: {\n                const input = ev.currentTarget;\n                let value = parseFloat(input.value || input.placeholder);\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                value += (ev.which === $.ui.keyCode.UP ? step : -step);\n                input.value = this._floatToStr(value);\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    custom_events: _.extend({}, SelectUserValueWidget.prototype.custom_events, {\n        'custom_color_picked': '_onCustomColorPicked',\n        'color_picked': '_onColorPicked',\n        'color_hover': '_onColorHovered',\n        'color_leave': '_onColorLeft',\n        'enter_key_color_colorpicker': '_onEnterKey'\n    }),\n\n    /**\n     * @override\n     */\n    start: async function () {\n        const _super = this._super.bind(this);\n        const args = arguments;\n\n        if (this.options.dataAttributes.lazyPalette === 'true') {\n            // TODO review in master, this was done in stable to keep the speed\n            // fix as stable as possible (to have a reference to a widget even\n            // if not a colorPalette widget).\n            this.colorPalette = new Widget(this);\n            this.colorPalette.getColorNames = () => [];\n            await this.colorPalette.appendTo(document.createDocumentFragment());\n        } else {\n            await this._renderColorPalette();\n        }\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        this.options.childNodes = [this.colorPalette.el];\n        this.options.valueEl = this.colorPreviewEl;\n\n        return _super(...args);\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    open: function () {\n        if (this.colorPalette.setSelectedColor) {\n            this.colorPalette.setSelectedColor(this._ccValue, this._value);\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        }\n        this._super(...arguments);\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 _.extend(this._super(...arguments), {\n            colorNames: this.colorPalette.getColorNames(),\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) && !ColorpickerWidget.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.colorPalette.getColorNames());\n        this.colorPreviewEl.classList.remove(...classes);\n        this.colorPreviewEl.style.removeProperty('background-color');\n        this.colorPreviewEl.style.removeProperty('background-image');\n        if (this._ccValue) {\n            this.colorPreviewEl.classList.add('o_cc', `o_cc${this._ccValue}`);\n        }\n        if (this._value) {\n            if (ColorpickerWidget.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 {\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        if (this.colorPalette.setSelectedColor) {\n            this.colorPalette.setSelectedColor(this._ccValue, this._value, false);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @returns {Promise}\n     */\n    _renderColorPalette: function () {\n        const options = {\n            selectedCC: this._ccValue,\n            selectedColor: this._value,\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 = 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 oldColorPalette = this.colorPalette;\n        this.colorPalette = new ColorPaletteWidget(this, options);\n        if (oldColorPalette) {\n            return this.colorPalette.insertAfter(oldColorPalette.el).then(() => {\n                oldColorPalette.destroy();\n            });\n        }\n        return this.colorPalette.appendTo(document.createDocumentFragment());\n    },\n    /**\n     * @override\n     */\n    _shouldIgnoreClick(ev) {\n        return ev.originalEvent.__isColorpickerClick || this._super(...arguments);\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 {Event} ev\n     */\n    _onCustomColorPicked: function (ev) {\n        this._customColorValue = ev.data.color;\n    },\n    /**\n     * Called when a color button is clicked -> confirms the preview.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorPicked: function (ev) {\n        this._previewCC = false;\n        this._previewColor = false;\n        this._customColorValue = false;\n\n        this._ccValue = ev.data.ccValue;\n        this._value = ev.data.color;\n\n        this._onUserValueChange(ev);\n    },\n    /**\n     * Called when a color button is entered -> previews the background color.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorHovered: function (ev) {\n        this._previewCC = ev.data.ccValue;\n        this._previewColor = ev.data.color;\n        this._onUserValuePreview(ev);\n    },\n    /**\n     * Called when a color button is left -> cancels the preview.\n     *\n     * @private\n     * @param {Event} ev\n     */\n    _onColorLeft: function (ev) {\n        this._previewCC = false;\n        this._previewColor = false;\n        this._onUserValueReset(ev);\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}) {\n        el.src = this._value;\n        const $editable = this.$target.closest('.o_editable');\n        const mediaDialog = new weWidgets.MediaDialog(this, {\n            noImages: !images,\n            noVideos: !videos,\n            noIcons: true,\n            noDocuments: true,\n            isForBgVideo: true,\n            vimeoPreviewIds: ['299225971', '414790269', '420192073', '368484050', '334729960', '417478345',\n                '312451183', '415226028', '367762632', '340475898', '374265101', '370467553'],\n            'res_model': $editable.data('oe-model'),\n            'res_id': $editable.data('oe-id'),\n        }, el).open();\n        return mediaDialog;\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        const dialog = this._openDialog(dummyEl, {images: true});\n        dialog.on('save', this, data => {\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 = dummyEl.getAttribute('src');\n            this._onUserValueChange();\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        const dialog = this._openDialog(dummyEl, {videos: true});\n        dialog.on('save', this, data => {\n            this._value = data.bgVideoSrc;\n            this._onUserValueChange();\n        });\n    },\n});\n\nconst DatetimePickerUserValueWidget = InputUserValueWidget.extend({\n    events: { // Explicitely not consider all InputUserValueWidget events\n        'blur input': '_onInputBlur',\n        'change.datetimepicker': '_onDateTimePickerChange',\n        'error.datetimepicker': '_onDateTimePickerError',\n        'input input': '_onDateInputInput',\n    },\n    defaultFormat: time.getLangDatetimeFormat(),\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super(...arguments);\n        this._value = moment().unix().toString();\n        this.__libInput = 0;\n    },\n    /**\n     * @override\n     */\n    start: async function () {\n        await this._super(...arguments);\n\n        const datetimePickerId = _.uniqueId('datetimepicker');\n        this.el.classList.add('o_we_large');\n        this.inputEl.classList.add('datetimepicker-input', 'mx-0', 'text-left');\n        this.inputEl.setAttribute('id', datetimePickerId);\n        this.inputEl.setAttribute('data-target', '#' + datetimePickerId);\n\n        const datepickersOptions = {\n            minDate: moment({ y: 1000 }),\n            maxDate: moment().add(200, 'y'),\n            calendarWeeks: true,\n            defaultDate: moment().format(),\n            icons: {\n                close: 'fa fa-check primary',\n            },\n            locale: moment.locale(),\n            format: this.defaultFormat,\n            sideBySide: true,\n            buttons: {\n                showClear: true,\n                showClose: true,\n                showToday: true,\n            },\n            widgetParent: 'body',\n\n            // Open the datetimepicker on focus not on click. This allows to\n            // take care of a bug which is due to the wysiwyg editor:\n            // sometimes, the datetimepicker loses the focus then get it back\n            // in the same execution flow. This was making the datepicker close\n            // for no apparent reason. Now, it only closes then reopens directly\n            // without it be possible to notice.\n            allowInputToggle: true,\n        };\n        this.__libInput++;\n        const $input = $(this.inputEl);\n        $input.datetimepicker(datepickersOptions);\n        this.__libInput--;\n\n        // Monkey-patch the library option to add custom classes on the pickers\n        const libObject = $input.data('datetimepicker');\n        const oldFunc = libObject._getTemplate;\n        libObject._getTemplate = function () {\n            const $template = oldFunc.call(this, ...arguments);\n            $template.addClass('o_we_no_overlay o_we_datetimepicker');\n            return $template;\n        };\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams: function () {\n        return _.extend(this._super(...arguments), {\n            format: this.defaultFormat,\n        });\n    },\n    /**\n     * @override\n     */\n    isPreviewed: function () {\n        return this._super(...arguments) || !!$(this.inputEl).data('datetimepicker').widget;\n    },\n    /**\n     * @override\n     */\n    async setValue() {\n        await this._super(...arguments);\n        let momentObj = null;\n        if (this._value) {\n            momentObj = moment.unix(this._value);\n            if (!momentObj.isValid()) {\n                momentObj = moment();\n            }\n        }\n        this.__libInput++;\n        $(this.inputEl).datetimepicker('date', momentObj);\n        this.__libInput--;\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     * @param {Event} ev\n     */\n    _onDateTimePickerChange: function (ev) {\n        if (this.__libInput > 0) {\n            return;\n        }\n        if (!ev.date || !ev.date.isValid()) {\n            this._value = '';\n        } else {\n            this._value = ev.date.unix().toString();\n        }\n        this._onUserValuePreview(ev);\n    },\n    /**\n     * Prevents crash manager to throw CORS error. Note that library already\n     * clears the wrong date format.\n     */\n    _onDateTimePickerError: function (ev) {\n        ev.stopPropagation();\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    defaultFormat: time.getLangDateFormat(),\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        'change table input': '_onListItemChange',\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    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    getMethodsParams() {\n        return _.extend(this._super(...arguments), {\n            records: this.records,\n        });\n    },\n    /**\n     * @override\n     */\n    setValue() {\n        this._super(...arguments);\n        const currentValues = 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 domains should be written the\n            // same way for the cache to work.\n            const selectedIdsDomain = selectedIds.length ? ['id', 'not in', selectedIds] : null;\n            const selectedIdsDomainIndex = this.createWidget.options.domain.findIndex(domain => domain[0] === 'id' && domain[1] === 'not in');\n            if (selectedIdsDomainIndex > -1) {\n                if (selectedIdsDomain) {\n                    this.createWidget.options.domain[selectedIdsDomainIndex] = selectedIdsDomain;\n                } else {\n                    this.createWidget.options.domain.splice(selectedIdsDomainIndex, 1);\n                }\n            } else {\n                if (selectedIdsDomain) {\n                    this.createWidget.options.domain = [...this.createWidget.options.domain, selectedIdsDomain];\n                }\n            }\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        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            for (const key of Object.keys(recordData)) {\n                inputEl.dataset[key] = recordData[key];\n            }\n        }\n        inputEl.disabled = !this.isCustom;\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        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)) {\n                checkboxEl.classList.add('active');\n            }\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        const buttonTdEl = document.createElement('td');\n        buttonTdEl.appendChild(buttonEl);\n        trEl.appendChild(buttonTdEl);\n        this.listTable.appendChild(trEl);\n    },\n    /**\n     * @private\n     */\n    _makeListItemsSortable() {\n        if (this.el.dataset.unsortable) {\n            return;\n        }\n        $(this.listTable).sortable({\n            axis: 'y',\n            handle: '.o_we_drag_handle',\n            items: 'tr',\n            cursor: 'move',\n            opacity: 0.6,\n            stop: (event, ui) => {\n                this._notifyCurrentState();\n            },\n        });\n    },\n    /**\n     * @private\n     */\n    _notifyCurrentState() {\n        const values = [...this.listTable.querySelectorAll('.o_we_list_record_name input')].map(el => {\n            const id = this.isCustom ? el.value : el.name;\n            return Object.assign({\n                id: /^-?[0-9]{1,15}$/.test(id) ? parseInt(id) : id,\n                name: el.value,\n                display_name: 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 = input.name || input.value;\n                return /^-?[0-9]{1,15}$/.test(id) ? parseInt(id) : id;\n            });\n            values.forEach(v => {\n                v.selected = this.selected.includes(v.id);\n            });\n        }\n        this._value = JSON.stringify(values);\n        this.notifyValueChange(false);\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        this._addItemToTable();\n        this._notifyCurrentState();\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     */\n    _onListItemChange() {\n        this._notifyCurrentState();\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        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\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        this.input.value = possibleValues.length > 1 ? possibleValues.indexOf(value) : this._value;\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 {Event} ev\n     */\n    _onInputInput(ev) {\n        this._value = ev.target.value;\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_we_pager_next, .o_we_pager_prev': '_onPageChange',\n    }),\n\n    /**\n     * @override\n     */\n    async start() {\n        const _super = this._super.bind(this);\n        this.pages = this.options.childNodes.filter(node => node.matches && node.matches('we-select-page'));\n        this.numPages = this.pages.length;\n\n        const prev = document.createElement('i');\n        prev.classList.add('o_we_pager_prev', 'fa', 'fa-chevron-left');\n\n        this.pageNum = document.createElement('span');\n        this.currentPage = 0;\n\n        const next = document.createElement('i');\n        next.classList.add('o_we_pager_next', 'fa', 'fa-chevron-right');\n\n        const pagerControls = document.createElement('div');\n        pagerControls.classList.add('o_we_pager_controls');\n        pagerControls.appendChild(prev);\n        pagerControls.appendChild(this.pageNum);\n        pagerControls.appendChild(next);\n\n        this.pageName = document.createElement('b');\n        const pagerHeader = document.createElement('div');\n        pagerHeader.classList.add('o_we_pager_header');\n        pagerHeader.appendChild(this.pageName);\n        pagerHeader.appendChild(pagerControls);\n\n        await _super(...arguments);\n        this.menuEl.classList.add('o_we_has_pager');\n        $(this.menuEl).prepend(pagerHeader);\n        this._updatePage();\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _shouldIgnoreClick(ev) {\n        return !!ev.target.closest('.o_we_pager_header') || this._super(...arguments);\n    },\n    /**\n     * Updates the pager's page number display.\n     *\n     * @private\n     */\n    _updatePage() {\n        this.pages.forEach((page, i) => page.classList.toggle('active', i === this.currentPage));\n        this.pageNum.textContent = `${this.currentPage + 1}/${this.numPages}`;\n        const activePage = this.pages.find((page, i) => i === this.currentPage);\n        this.pageName.textContent = activePage.getAttribute('string');\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * Goes to the previous/next page with wrap-around.\n     *\n     * @private\n     */\n    _onPageChange(ev) {\n        ev.preventDefault();\n        ev.stopPropagation();\n        const delta = ev.target.matches('.o_we_pager_next') ? 1 : -1;\n        this.currentPage = (this.currentPage + this.numPages + delta) % this.numPages;\n        this._updatePage();\n    },\n    /**\n     * @override\n     */\n    _onClick(ev) {\n        const activeButton = this._getActiveSubWidget();\n        if (activeButton) {\n            const currentPage = this.pages.indexOf(activeButton.el.closest('we-select-page'));\n            if (currentPage !== -1) {\n                this.currentPage = currentPage;\n                this._updatePage();\n            }\n        }\n        return this._super(...arguments);\n    },\n});\n\nconst m2oRpcCache = {};\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    configAttributes: ['model', 'fields', 'limit', 'domain', 'callWith', 'createMethod'],\n\n    /**\n     * @override\n     */\n    init(parent, title, options, $target) {\n        this.afterSearch = [];\n        this.displayNameCache = {};\n        this._rpcCache = m2oRpcCache;\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        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 === '/') {\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 = _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    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Caches the rpc.\n     *\n     * @override\n     */\n    async _rpc() {\n        const cacheId = JSON.stringify(...arguments);\n        if (!this._rpcCache[cacheId]) {\n            this._rpcCache[cacheId] = this._super(...arguments);\n        }\n        return this._rpcCache[cacheId];\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._rpc({\n            model: this.options.model,\n            method: 'name_search',\n            kwargs: {\n                name: needle,\n                args: await this._getSearchDomain(),\n                operator: \"ilike\",\n                limit: this.options.limit + 1,\n            },\n        });\n        const records = await this._rpc({\n            model: this.options.model,\n            method: 'read',\n            args: [recTuples.map(([id, _name]) => id), 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        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    },\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._rpc({\n                model: this.options.model,\n                method: 'read',\n                args: [[recordId], ['display_name']],\n            }))[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.which !== $.ui.keyCode.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'],\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        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._rpc({\n            model: model,\n            method: 'read',\n            args: [[parseInt(recordId)], [m2oField]],\n        });\n        const selectedRecordIds = record[m2oField];\n        // TODO: handle no record\n        const modelData = await this._rpc({\n            model: model,\n            method: 'fields_get',\n            args: [[m2oField]],\n        });\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._rpc({\n            model: this.m2oModel,\n            method: 'read',\n            args: [selectedRecordIds, ['display_name']],\n        });\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     * @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 = 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.\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    /**\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    /**\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     */\n    onBuilt: function () {},\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     * 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 always set as important when applied.\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            const combined = backgroundImagePartsToCss(bgImageParts);\n            this.$target[0].style.setProperty('background-image', '');\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        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        function applyCSS(cssProp, cssValue, styles) {\n            const forceStyle = (typeof params.forceStyle !== 'undefined');\n            if (forceStyle\n                    || !weUtils.areCssValuesEqual(styles[cssProp], cssValue, cssProp, this.$target[0])) {\n                const priority = forceStyle ? params.forceStyle : 'important';\n                this.$target[0].style.setProperty(cssProp, cssValue, priority);\n                return true;\n            }\n            return false;\n        }\n\n        if (params.extraClass) {\n            this.$target.toggleClass(params.extraClass, hasUserValue);\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        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     * @returns {Promise}\n     */\n    updateUI: async function ({noVisibility} = {}) {\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            el.classList.toggle('d-none', !$(el).find('> div > .o_we_user_value_widget').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\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                const _restoreTransitions = () => this.$target[0].classList.remove('o_we_force_no_transition');\n\n                const styles = window.getComputedStyle(this.$target[0]);\n\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 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                        _restoreTransitions();\n                        return parts.gradient;\n                    }\n                }\n\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[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                _restoreTransitions();\n\n                const value = cssValues.join(' ');\n\n                if (params.cssProperty === 'background-color' && params.withCombinations) {\n                    if (usedCC) {\n                        const ccValue = 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 = ColorpickerWidget.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\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        if (widgetName === 'move_up_opt' || widgetName === 'move_left_opt') {\n            return !this.$target.is(':first-child');\n        }\n        if (widgetName === 'move_down_opt' || widgetName === 'move_right_opt') {\n            return !this.$target.is(':last-child');\n        }\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 = ColorpickerWidget.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()) {\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 = _.map($applyTo, 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                    Dialog.confirm(this, warnMessage, {\n                        confirm_callback: () => resolve(true),\n                        cancel_callback: () => resolve(false),\n                    });\n                });\n                if (!okWarning) {\n                    return;\n                }\n            }\n\n            const reloadMessage = await this._checkIfWidgetsUpdateNeedReload(widgets);\n            requiresReload = !!reloadMessage;\n            if (requiresReload) {\n                const save = await new Promise(resolve => {\n                    Dialog.confirm(this, _t(\"To apply this change, we need to save all your previous modifications and reload the page.\") + ' '\n                            + (typeof reloadMessage === 'string' ? reloadMessage : ''), {\n                        buttons: [{\n                            text: _t('Save and Reload'),\n                            classes: 'btn-primary',\n                            close: true,\n                            click: () => resolve(true),\n                        }, {\n                            text: _t(\"Cancel\"),\n                            close: true,\n                            click: () => resolve(false)\n                        }],\n                    });\n                });\n                if (!save) {\n                    return;\n                }\n            }\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        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) {\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        }});\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        const linkedWidgets = this._requestUserValueWidgets(...ev.data.triggerWidgetsNames);\n        if (linkedWidgets.length !== ev.data.triggerWidgetsNames.length) {\n            console.warn('Missing widget to trigger');\n            return;\n        }\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            });\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        var self = this;\n        var def = this._super.apply(this, arguments);\n\n        this.$handles = this.$overlay.find('.o_handle');\n\n        var resizeValues = this._getSize();\n        this.$handles.on('mousedown', function (ev) {\n            ev.preventDefault();\n            self.options.wysiwyg.odooEditor.automaticStepUnactive('resizing');\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            var $handle = $(ev.currentTarget);\n\n            var compass = false;\n            var 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            }\n\n            var resize = resizeValues[compass];\n            if (!resize) {\n                return;\n            }\n\n            var current = 0;\n            var cssProperty = resize[2];\n            var cssPropertyValue = parseInt(self.$target.css(cssProperty));\n            _.each(resize[0], function (val, key) {\n                if (self.$target.hasClass(val)) {\n                    current = key;\n                } else if (resize[1][key] === cssPropertyValue) {\n                    current = key;\n                }\n            });\n            var begin = current;\n            var beginClass = self.$target.attr('class');\n            var regClass = new RegExp('\\\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');\n\n            var cursor = $handle.css('cursor') + '-important';\n            var $body = $(this.ownerDocument.body);\n            $body.addClass(cursor);\n\n            var xy = ev['page' + XY];\n            var bodyMouseMove = function (ev) {\n                ev.preventDefault();\n\n                var dd = ev['page' + XY] - xy + resize[1][begin];\n                var next = current + (current + 1 === resize[1].length ? 0 : 1);\n                var prev = current ? (current - 1) : 0;\n\n                var change = false;\n                if (dd > (2 * resize[1][next] + resize[1][current]) / 3) {\n                    self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));\n                    self.$target.addClass(resize[0][next]);\n                    current = next;\n                    change = true;\n                }\n                if (prev !== current && dd < (2 * resize[1][prev] + resize[1][current]) / 3) {\n                    self.$target.attr('class', (self.$target.attr('class') || '').replace(regClass, ''));\n                    self.$target.addClass(resize[0][prev]);\n                    current = prev;\n                    change = true;\n                }\n\n                if (change) {\n                    self._onResize(compass, beginClass, current);\n                    self.trigger_up('cover_update');\n                    $handle.addClass('o_active');\n                }\n            };\n            var bodyMouseUp = function () {\n                $body.off('mousemove', bodyMouseMove);\n                $body.off('mouseup', bodyMouseUp);\n                $body.removeClass(cursor);\n                $handle.removeClass('o_active');\n\n                // Highlights the previews for a while\n                var $handlers = self.$overlay.find('.o_handle');\n                $handlers.addClass('o_active').delay(300).queue(function () {\n                    $handlers.removeClass('o_active').dequeue();\n                });\n\n                if (begin === current) {\n                    return;\n                }\n                setTimeout(function () {\n                    self.options.wysiwyg.odooEditor.historyStep();\n                }, 0);\n\n                self.options.wysiwyg.odooEditor.automaticStepActive('resizing');\n            };\n            $body.on('mousemove', bodyMouseMove);\n            $body.on('mouseup', bodyMouseUp);\n        });\n\n        _.each(resizeValues, (value, key) => {\n            this.$handles.filter('.' + key).toggleClass('readonly', !value);\n        });\n\n        return def;\n    },\n    /**\n     * @override\n     */\n    onFocus: function () {\n        this._onResize();\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    setTarget: function () {\n        this._super(...arguments);\n        this._onResize();\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        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        _.each(resizeValues, function (resizeValue, direction) {\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            _.each(classes, function (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 the left, top and bottom sizes\n        var ml = this.$target.css('margin-left');\n        this.$overlay.find('.o_handle.w').css({\n            width: ml,\n            left: '-' + ml,\n        });\n        this.$overlay.find('.o_handle.e').css({\n            width: 0,\n        });\n        _.each(this.$overlay.find(\".o_handle.n, .o_handle.s\"), function (handle) {\n            var $handle = $(handle);\n            var direction = $handle.hasClass('n') ? 'top' : 'bottom';\n            $handle.height(self.$target.css('padding-' + direction));\n        });\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            var _class = this.$target.attr('class').replace(/\\s*(offset-xl-|offset-lg-)([0-9-]+)/g, '');\n            this.$target.attr('class', _class);\n        }\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _getSize: function () {\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: [_.map(gridE, v => ('col-lg-' + v)), _.map(gridE, v => width / 12 * v), 'width'],\n            w: [_.map(gridW, v => ('offset-lg-' + v)), _.map(gridW, v => width / 12 * v), 'margin-left'],\n        };\n        return this.grid;\n    },\n    /**\n     * @override\n     */\n    _onResize: function (compass, beginClass, current) {\n        if (compass === 'w' || compass === 'e') {\n            const beginOffset = Number(beginClass.match(/offset-lg-([0-9-]+)|$/)[1] || beginClass.match(/offset-xl-([0-9-]+)|$/)[1] || 0);\n\n            if (compass === 'w') {\n                // don't change the right border position when we change the offset (replace col size)\n                var beginCol = Number(beginClass.match(/col-lg-([0-9]+)|$/)[1] || 0);\n                var offset = Number(this.grid.w[0][current].match(/offset-lg-([0-9-]+)|$/)[1] || 0);\n                if (offset < 0) {\n                    offset = 0;\n                }\n                var colSize = beginCol - (offset - beginOffset);\n                if (colSize <= 0) {\n                    colSize = 1;\n                    offset = beginOffset + beginCol - 1;\n                }\n                this.$target.attr('class', this.$target.attr('class').replace(/\\s*(offset-xl-|offset-lg-|col-lg-)([0-9-]+)/g, ''));\n\n                this.$target.addClass('col-lg-' + (colSize > 12 ? 12 : colSize));\n                if (offset > 0) {\n                    this.$target.addClass('offset-lg-' + offset);\n                }\n            } else if (beginOffset > 0) {\n                const endCol = Number(this.grid.e[0][current].match(/col-lg-([0-9]+)|$/)[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                    this.$target[0].className = this.$target[0].className.replace(/\\s*(col-lg-)([0-9-]+)/g, '');\n                    this.$target[0].classList.add('col-lg-' + (12 - beginOffset));\n                }\n            }\n        }\n        this._super.apply(this, arguments);\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) || 'none';\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        const el = document.createElement('div');\n        if (type) {\n            el.classList.add(shadowClass);\n        }\n\n        let shadow = ''; // TODO in master this should be changed to 'none'\n        document.body.appendChild(el);\n        switch (type) {\n            case 'outset': {\n                shadow = $(el).css('box-shadow');\n                break;\n            }\n            case 'inset': {\n                shadow = $(el).css('box-shadow') + ' inset';\n                break;\n            }\n        }\n        el.remove();\n        return shadow;\n    }\n});\n\n\n\nregistry.layout_column = SnippetOptionWidget.extend({\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        const previousNbColumns = this.$('> .row').children().length;\n        let $row = this.$('> .row');\n        if (!$row.length) {\n            const restoreCursor = preserveCursor(this.$target[0].ownerDocument);\n            for (const node of descendants(this.$target[0])) {\n                node.ouid = undefined;\n            }\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, (nbColumns || 1) - $row.children().length);\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            for (const node of descendants($row[0])) {\n                node.ouid = undefined;\n            }\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    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _computeWidgetState: function (methodName, params) {\n        if (methodName === 'selectCount') {\n            return this.$('> .row').children().length;\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        }\n        return this._super(...arguments);\n    },\n    /**\n     * Adds new columns which are clones of the last column or removes the\n     * last x columns.\n     *\n     * @private\n     * @param {jQuery} $row - the row in which to update the columns\n     * @param {integer} count - positif to add, negative to remove\n     */\n    _updateColumnCount: async function ($row, count) {\n        if (!count) {\n            return;\n        }\n\n        if (count > 0) {\n            var $lastColumn = $row.children().last();\n            for (var i = 0; i < count; i++) {\n                await new Promise(resolve => {\n                    this.trigger_up('clone_snippet', {$snippet: $lastColumn, onSuccess: resolve});\n                });\n            }\n        } else {\n            var self = this;\n            for (const el of $row.children().slice(count)) {\n                await new Promise(resolve => {\n                    self.trigger_up('remove_snippet', {$snippet: $(el), onSuccess: resolve, shouldRecordUndo: false});\n                });\n            }\n        }\n\n        this._resizeColumns($row.children());\n        this.trigger_up('cover_update');\n    },\n    /**\n     * Resizes the columns so that they are kept on one row.\n     *\n     * @private\n     * @param {jQuery} $columns - the columns to resize\n     */\n    _resizeColumns: function ($columns) {\n        const colsLength = $columns.length;\n        var colSize = Math.floor(12 / colsLength) || 1;\n        var colOffset = Math.floor((12 - colSize * colsLength) / 2);\n        var colClass = 'col-lg-' + colSize;\n        _.each($columns, function (column) {\n            var $column = $(column);\n            $column.attr('class', $column.attr('class').replace(/\\b(col|offset)-lg(-\\d+)?\\b/g, ''));\n            $column.addClass(colClass);\n        });\n        if (colOffset) {\n            $columns.first().addClass('offset-lg-' + colOffset);\n        }\n    },\n});\n\n/**\n * Allows snippets to be moved before the preceding element or after the following.\n */\nregistry.SnippetMove = SnippetOptionWidget.extend({\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        $overlayArea.prepend($buttons[0]);\n        $overlayArea.append($buttons[1]);\n\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    onFocus: function () {\n        // TODO improve this: hack to hide options section if snippet move is\n        // the only one.\n        const $allOptions = this.$el.parent();\n        if ($allOptions.find('we-customizeblock-option').length <= 1) {\n            $allOptions.addClass('d-none');\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 isNavItem = this.$target[0].classList.contains('nav-item');\n        const $tabPane = isNavItem ? $(this.$target.find('.nav-link')[0].hash) : null;\n        switch (widgetValue) {\n            case 'prev':\n                this.$target.prev().before(this.$target);\n                if (isNavItem) {\n                    $tabPane.prev().before($tabPane);\n                }\n                break;\n            case 'next':\n                this.$target.next().after(this.$target);\n                if (isNavItem) {\n                    $tabPane.next().after($tabPane);\n                }\n                break;\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                    easing: 'linear',\n                    duration: 500,\n                });\n            }\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/**\n * Allows for media to be replaced.\n */\nregistry.ReplaceMedia = SnippetOptionWidget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/image_link_tools.xml'],\n\n    /**\n     * @override\n     */\n    onFocus() {\n        core.bus.on('activate_image_link_tool', this, this._activateLinkTool);\n        core.bus.on('deactivate_image_link_tool', this, 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        core.bus.off('activate_image_link_tool', this, this._activateLinkTool);\n        core.bus.off('deactivate_image_link_tool', this, this._deactivateLinkTool);\n    },\n\n    //--------------------------------------------------------------------------\n    // Options\n    //--------------------------------------------------------------------------\n\n    /**\n     * Replaces the media.\n     *\n     * @see this.selectClass for parameters\n     */\n    async replaceMedia() {\n        // TODO for now, this simulates a double click on the media,\n        // to be refactored when the new editor is merged\n        this.$target.dblclick();\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.$target[0].parentNode;\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.$target[0].parentElement;\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.$target[0].parentElement;\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        if (this.$target[0].parentElement.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    _deactivateLinkTool() {\n        const parentEl = this.$target[0].parentNode;\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.$target[0].parentElement;\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            }\n            return !this.$target[0].classList.contains('media_iframe_video');\n        }\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    async _renderCustomXML(uiFragment) {\n        const rowEl = uiFragment.querySelector('we-row');\n        rowEl.insertAdjacentHTML('beforeend', qweb.render('web_editor.media_link_tools_button'));\n        rowEl.insertAdjacentHTML('afterend', qweb.render('web.editor.media_link_tools_fields'));\n    },\n});\n\n/*\n * Abstract option to be extended by the ImageOptimize 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        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    selectWidth(previewMode, widgetValue, params) {\n        this._getImg().dataset.resizeWidth = widgetValue;\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 'selectWidth':\n                return img.naturalWidth;\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._isAllowedOnAllImages()) {\n            return;\n        }\n        if (!this.originalSrc || !this._isImageSupportedForProcessing(img)) {\n            [...uiFragment.childNodes].forEach(node => node.remove());\n            return;\n        }\n        const $select = $(uiFragment).find('we-select[data-name=width_select_opt]');\n        (await this._computeAvailableWidths()).forEach(([value, label]) => {\n            $select.append(`<we-button data-select-width=\"${value}\">${label}</we-button>`);\n        });\n\n        if (this._getImageMimetype(img) !== 'image/jpeg') {\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 widths for a given image.\n     *\n     * @private\n     */\n    async _computeAvailableWidths() {\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',\n            256: '256px',\n            512: '512px',\n            1024: '1024px',\n            1920: '1920px',\n        };\n        widths[img.naturalWidth] = _.str.sprintf(_t(\"%spx\"), img.naturalWidth);\n        widths[optimizedWidth] = _.str.sprintf(_t(\"%dpx (Suggested)\"), optimizedWidth);\n        widths[maxWidth] = _.str.sprintf(_t(\"%dpx (Original)\"), maxWidth);\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        const dataURL = await applyModifications(img, {mimetype: this._getImageMimetype(img)});\n        this._filesize = dataURL.split(',')[1].length / 4 * 3 / 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            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, this._rpc.bind(this), 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    },\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        this._getImg().dataset.resizeWidth = this.optimizedWidth;\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     * @returns {Boolean}\n     */\n    _isImageSupportedForProcessing(img) {\n        return isImageSupportedForProcessing(this._getImageMimetype(img));\n    },\n    /**\n     * TODO: adapt in master (used to keep ImageTools related options available\n     * for all supported images).\n     *\n     * @returns {Boolean}\n     */\n    _isAllowedOnAllImages() {\n        return isImageSupportedForStyle(this._getImg());\n    },\n});\n\n/**\n * @param {Element} containerEl\n * @returns {Element}\n */\nconst _addAnimatedShapeLabel = function addAnimatedShapeLabel(containerEl) {\n    const labelEl = document.createElement('span');\n    labelEl.classList.add('o_we_shape_animated_label');\n    const labelStr = _t(\"Animated\");\n    labelEl.textContent = labelStr[0];\n    const spanEl = document.createElement('span');\n    spanEl.textContent = labelStr.substr(1);\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     * Displays the image cropping tools\n     *\n     * @see this.selectClass for parameters\n     */\n    async crop() {\n        this.trigger_up('hide_overlay');\n        this.trigger_up('disable_loading_effect');\n        new weWidgets.ImageCropWidget(this, this.$target[0]).appendTo(this.options.wysiwyg.odooEditor.document.body);\n\n        await new Promise(resolve => {\n            this.$target.one('image_cropper_destroyed', async () => {\n                await this._reapplyCurrentShape();\n                resolve();\n            });\n        });\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 mousedown = mousedownEvent => {\n            if (!$(mousedownEvent.target).closest('.transfo-container').length) {\n                this.$target.transfo('destroy');\n                $(document).off('mousedown', mousedown);\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\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 cropper = new weWidgets.ImageCropWidget(this, this.$target[0]);\n        await cropper.appendTo(this.options.wysiwyg.odooEditor.document.body);\n        await cropper.reset();\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        const img = this._getImg();\n        const saveData = previewMode === false;\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            }\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            if (saveData) {\n                img.dataset.mimetype = img.dataset.originalMimetype;\n                delete img.dataset.originalMimetype;\n            }\n        }\n        img.classList.add('o_modified_image_to_save');\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    //--------------------------------------------------------------------------\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    },\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\n        const svg = new DOMParser().parseFromString(svgText, 'image/svg+xml').documentElement;\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 imgDataURL = await applyModifications(img, {mimetype: this._getImageMimetype(img)});\n        svg.removeChild(svg.querySelector('#preview'));\n        svg.querySelector('image').setAttribute('xlink:href', imgDataURL);\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(img.src);\n        svg.setAttribute('width', originalImage.naturalWidth);\n        svg.setAttribute('height', originalImage.naturalHeight);\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\n        const reader = new FileReader();\n        const readPromise = new Promise(resolve => {\n            reader.addEventListener('load', () => resolve(reader.result));\n        });\n        reader.readAsDataURL(blob);\n        const dataURL = await readPromise;\n        const imgFilename = (img.dataset.originalSrc.split('/').pop()).split('.')[0];\n        img.dataset.fileName = `${imgFilename}.svg`;\n        return loadImage(dataURL, img);\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(utils.confine(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(utils.confine(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        if (!this.$overlay.data('$optionsSection')) {\n            return;\n        }\n        const leftPanelEl = this.$overlay.data('$optionsSection')[0];\n        const titleTextEl = leftPanelEl.querySelector('we-title > span');\n        const weightEl = titleTextEl.querySelector('.o_we_image_weight');\n        if (weightEl) {\n            weightEl.remove();\n        }\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            if (!shapeName) {\n                return false;\n            }\n            const colors = img.dataset.shapeColors.split(';');\n            return colors[parseInt(params.colorId)];\n        }\n        if (params.optionsPossibleValues.resetTransform) {\n            return this._isTransformed();\n        }\n        if (params.optionsPossibleValues.resetCrop) {\n            return this._isCropped();\n        }\n        // Only the 'Crop' widget is visible when image is not supported for style options.\n        if (Object.keys(params.optionsPossibleValues).some(methodName => ['transform', 'selectStyle'].includes(methodName))) {\n            return isImageSupportedForStyle(this._getImg());\n        }\n        return this._super();\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        }\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            }\n        });\n    },\n    /**\n     * @override\n     */\n    _getImageMimetype(img) {\n        if (img.dataset.shape) {\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 || ColorpickerWidget.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 img = this._getImg();\n        const match = img.src.match(/\\/web_editor\\/image_shape\\/(\\w+\\.\\w+)/);\n        if (img.dataset.shape && match) {\n            return this._loadImageInfo(`/web/image/${encodeURIComponent(match[1])}`);\n        }\n        return this._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 && img.dataset.mimetype !== 'image/svg+xml') {\n            img.dataset.originalMimetype = img.dataset.mimetype;\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                return;\n            }\n            // Image data-mimetype should be changed to SVG since loadImageInfo()\n            // will set the original attachment mimetype on it.\n            img.dataset.mimetype = 'image/svg+xml';\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        }\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// TODO: adapt in master to only use ImageTools.\nregistry.ImageOptimize = registry.ImageTools.extend({\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _isAllowedOnAllImages() {\n        return false;\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        // Hack to align on the right\n        this.$weight.css({\n            'width': '200px', // Make parent row grow by faking a width\n            'flex': '0 0 0', // But force no forced width\n            'margin-left': 'auto',\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 && ColorpickerWidget.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 && src.pathname.startsWith('/web_editor/shape/');\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.removeClass('oe_img_bg o_bg_img_center');\n        }\n        const combined = backgroundImagePartsToCss(parts);\n        this.$target.css('background-image', combined);\n    },\n});\n\n/**\n * Handles background shapes.\n */\nregistry.BackgroundShape = SnippetOptionWidget.extend({\n    /**\n     * @override\n     */\n    onBuilt() {\n        this._patchShape(this.$target[0]);\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    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    updateUI() {\n        if (this.rerender) {\n            this.rerender = false;\n            return this._rerenderXML();\n        }\n        return this._super.apply(this, arguments);\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            };\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    //--------------------------------------------------------------------------\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        }\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        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            shapeEl.classList.add(`o_${shape.replace(/\\//g, '_')}`);\n            btn.append(btnContent);\n        });\n        return uiFragment;\n    },\n    /**\n     * @override\n     */\n    async _computeWidgetVisibility(widgetName, params) {\n        if (widgetName === 'shape_none_opt') {\n            return false;\n        }\n        return this._super(...arguments);\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'} = 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\n        if (colors || flip.length) {\n            // Custom colors/flip, 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        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} = 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=${flip.sort().join('')}`);\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        };\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(document.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_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     * @todo remove me in master, needed to patch errors on set-up shapes in\n     * themes.\n     *\n     * @param {HTMLElement} el\n     * @returns {Object}\n     */\n    _patchShape(el) {\n        const shapeData = this._getShapeData(el);\n        // Wrong shape data for s_picture in kea theme\n        if (shapeData.shape === 'web_editor/Origins/Wavy_03') {\n            shapeData.shape = 'web_editor/Wavy/03';\n            el.dataset.oeShapeData = JSON.stringify(shapeData);\n        }\n        return shapeData;\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 shapeData = this._patchShape(previousSibling);\n                const previousShape = shapeData.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            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, () => ({shape: shapeToSelect, colors: this._getImplicitColors(shapeToSelect)}));\n        }\n    },\n});\n\n/**\n * Handles the edition of snippets' background image position.\n */\nregistry.BackgroundPosition = SnippetOptionWidget.extend({\n    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],\n\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', '');\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 = $(qweb.render('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 = $('#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            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.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    },\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    /**\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._rpc({\n                model: oeMany2oneModel,\n                method: 'read',\n                args: [[parseInt(oeMany2oneId)], this.fields],\n            }).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._rpc({\n            model: 'ir.model',\n            method: 'search_read',\n            args: [[['model', '=', model]], ['name']],\n        });\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._rpc({\n                        model: 'ir.qweb.field.contact',\n                        method: 'get_record_to_html',\n                        args: [[contactId]],\n                        kwargs: {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    xmlDependencies: ['/web_editor/static/src/xml/snippets.xml'],\n\n    /**\n     * @override\n     */\n    start: function () {\n        this.trigger_up('get_snippet_versions', {\n            snippetName: this.$target[0].dataset.snippet,\n            onSuccess: snippetVersions => {\n                const isUpToDate = snippetVersions && ['vjs', 'vcss', 'vxml'].every(key => this.$target[0].dataset[key] === snippetVersions[key]);\n                if (!isUpToDate) {\n                    this.$el.prepend(qweb.render('web_editor.outdated_block_message'));\n                }\n            },\n        });\n        return this._super(...arguments);\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    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],\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            Dialog.confirm(this, _t(\"To save a snippet, we need to save all your previous modifications and reload the page.\"), {\n                cancel_callback: () => resolve(false),\n                buttons: [\n                    {\n                        text: _t(\"Save and Reload\"),\n                        classes: 'btn-primary',\n                        close: true,\n                        click: () => {\n                            const snippetKey = this.$target[0].dataset.snippet;\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                            this.trigger_up('request_save', {\n                                reloadEditor: true,\n                                onSuccess: async () => {\n                                    const defaultSnippetName = _.str.sprintf(_t(\"Custom %s\"), this.data.snippetName);\n                                    const targetCopyEl = this.$target[0].cloneNode(true);\n                                    delete targetCopyEl.dataset.name;\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                                    await rpc.query({\n                                        model: 'ir.ui.view',\n                                        method: 'save_snippet',\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                        text: _t(\"Cancel\"),\n                        close: true,\n                        click: () => resolve(false),\n                    }\n                ]\n            });\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * TODO adapt in master, this option should only be instantiated for real\n     * snippets in the first place.\n     *\n     * @override\n     */\n    _computeVisibility() {\n        return this.$target[0].hasAttribute('data-snippet');\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^='/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    },\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._rpc({\n                model: 'ir.ui.view',\n                method: 'render_public_asset',\n                args: [`${xmlid}`, {}],\n                kwargs: {\n                    context: this.options.context,\n                },\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\nreturn {\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};\n});\n", "odoo.define('web_editor.wysiwyg', function (require) {\n'use strict';\n\nconst dom = require('web.dom');\nconst core = require('web.core');\nconst session = require('web.session');\nconst Widget = require('web.Widget');\nconst Dialog = require('web.Dialog');\nconst customColors = require('web_editor.custom_colors');\nconst {ColorPaletteWidget} = require('web_editor.ColorPalette');\nconst {ColorpickerWidget} = require('web.Colorpicker');\nconst concurrency = require('web.concurrency');\nconst { device } = require('web.config');\nconst weContext = require('web_editor.context');\nconst { localization } = require('@web/core/l10n/localization');\nconst OdooEditorLib = require('@web_editor/../lib/odoo-editor/src/OdooEditor');\nconst snippetsEditor = require('web_editor.snippet.editor');\nconst Toolbar = require('web_editor.toolbar');\nconst weWidgets = require('wysiwyg.widgets');\nconst Link = require('wysiwyg.widgets.Link');\nconst wysiwygUtils = require('@web_editor/js/common/wysiwyg_utils');\nconst weUtils = require('web_editor.utils');\nconst { PeerToPeer } = require('@web_editor/js/wysiwyg/PeerToPeer');\nconst { Mutex } = require('web.concurrency');\n\nvar _t = core._t;\nconst QWeb = core.qweb;\n\nconst OdooEditor = OdooEditorLib.OdooEditor;\nconst getDeepRange = OdooEditorLib.getDeepRange;\nconst getInSelection = OdooEditorLib.getInSelection;\nconst isBlock = OdooEditorLib.isBlock;\nconst rgbToHex = OdooEditorLib.rgbToHex;\nconst preserveCursor = OdooEditorLib.preserveCursor;\nconst closestElement = OdooEditorLib.closestElement;\nconst setSelection = OdooEditorLib.setSelection;\nconst endPos = OdooEditorLib.endPos;\n\nvar id = 0;\nconst faZoomClassRegex = RegExp('fa-[0-9]x');\nconst basicMediaSelector = 'img, .fa, .o_image, .media_iframe_video';\n// TODO review in master (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\nconst Wysiwyg = Widget.extend({\n    xmlDependencies: [\n    ],\n    defaultOptions: {\n        lang: 'odoo',\n        colors: customColors,\n        recordInfo: {context: {}},\n        document: document,\n        allowCommandVideo: true,\n    },\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        this.id = ++id;\n        this.options = options;\n        // autohideToolbar is true by default (false by default if navbar present).\n        this.options.autohideToolbar = typeof this.options.autohideToolbar === 'boolean'\n            ? this.options.autohideToolbar\n            : !options.snippets;\n        this.saving_mutex = new concurrency.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-toggle=\"tab\"])'\n            + ':not([data-toggle=\"collapse\"])'\n            + ':not([data-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        Wysiwyg.activeWysiwygs.add(this);\n        this._oNotEditableObservers = new Map();\n    },\n    /**\n     *\n     * @override\n     */\n    start: async function () {\n        const _super = this._super;\n        const self = this;\n\n        var options = this._editorOptions();\n        this.options.isInternalUser = await session.user_has_group('base.group_user');\n\n        this.$editable = this.$editable || this.$el;\n        if (options.value) {\n            this.$editable.html(options.value);\n        }\n        const initialHistoryId = options.value && this._getInitialHistoryId(options.value);\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.$editable.on('blur', this._onBlur);\n\n        this.toolbar = new Toolbar(this, this.options.toolbarTemplate);\n        await this.toolbar.appendTo(document.createElement('void'));\n        const commands = this._getCommands();\n\n        let editorCollaborationOptions;\n        if (\n            options.collaborationChannel &&\n            // Hack: check if mail module is installed.\n            this.getSession()['notification_type']\n        ) {\n            editorCollaborationOptions = this.setupCollaboration(options.collaborationChannel);\n        }\n\n        const getYoutubeVideoElement = (url) => {\n            const videoWidget = new weWidgets.VideoWidget(this, undefined, {});\n            const src = videoWidget._createVideoNode(url).$video.attr('src');\n            return videoWidget.getWrappedIframe(src)[0];\n        };\n\n        this.odooEditor = new OdooEditor(this.$editable[0], Object.assign({\n            _t: _t,\n            toolbar: this.toolbar.$el[0],\n            document: this.options.document,\n            autohideToolbar: !!this.options.autohideToolbar,\n            isRootEditable: this.options.isRootEditable,\n            onPostSanitize: this._setONotEditable.bind(this),\n            placeholder: this.options.placeholder,\n            showEmptyElementHint: this.options.showEmptyElementHint,\n            controlHistoryFromDocument: this.options.controlHistoryFromDocument,\n            initialHistoryId,\n            getContentEditableAreas: this.options.getContentEditableAreas,\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');\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 baseNode = closestElement(selection.anchorNode, 'P, DIV');\n                    const fieldContainer = closestElement(selection.anchorNode, '[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                    return !(\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                if (this.linkTools) {\n                    this.linkTools.destroy();\n                    this.linkTools = undefined;\n                }\n            },\n            commands: commands,\n            onChange: options.onChange,\n            plugins: options.editorPlugins,\n            direction: localization.direction || 'ltr',\n            renderingClasses: ['o_dirty', 'o_transform_removal', 'oe_edited_link', 'o_menu_loading'],\n        }, editorCollaborationOptions));\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        this.odooEditor.addEventListener('contentChanged', function () {\n            self.$editable.trigger('content_changed');\n            self.trigger_up('wysiwyg_change');\n        });\n\n        this._initialValue = this.getValue();\n        const $wrapwrap = $('#wrapwrap');\n        if ($wrapwrap.length) {\n            $wrapwrap[0].addEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true });\n            this.$root = this.$root || $wrapwrap;\n        }\n\n        if (this._peerToPeerLoading) {\n            // Now that the editor is loaded, wait for the peerToPeer to be\n            // ready to join.\n            this._peerToPeerLoading.then(() => this.ptp.notifyAllClients('ptp_join'));\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        this.showTooltip = true;\n        this.$editable.on('dblclick', mediaSelector, function () {\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                this.isContentEditable ||\n                // For a media to be editable, the base case is to be in a\n                // container whose content is editable.\n                (this.parentElement && this.parentElement.isContentEditable);\n\n            if (!isEditable && this.classList.contains('o_editable_media')) {\n                isEditable = weUtils.shouldEditableMediaBeEditable(this);\n            }\n\n            if (isEditable) {\n                self.showTooltip = false;\n                const $el = $(this);\n                const params = {node: $el};\n                $el.selectElement();\n\n                if ($el.is('.fa')) {\n                    // save layouting classes from icons to not break the page if you edit an icon\n                    params.htmlClass = [...$el[0].classList].filter((className) => {\n                        return !className.startsWith('fa') || faZoomClassRegex.test(className);\n                    }).join(' ');\n                }\n\n                self.openMediaDialog(params);\n            }\n        });\n\n        if (options.snippets) {\n            $(this.odooEditor.document.body).addClass('editor_enable');\n            this.snippetsMenu = this._createSnippetsMenuInstance(options);\n            await this._insertSnippetMenu();\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()).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. See\n        // _createPalette.\n        this._configureToolbar(options);\n\n        $(this.odooEditor.editable).on('click', 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.$root.on('click', (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                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                        if (this.snippetsMenu) {\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.snippetsMenu._mutex.exec(() => null);\n                        }\n                        this.linkPopover = await weWidgets.LinkPopoverWidget.createFor(this, $target[0], { wysiwyg: this });\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, .iframe-editor-wrapper').length && this.snippetsMenu) {\n                    this.toggleLinkTools({\n                        forceOpen: true,\n                        link: $target[0],\n                        noFocusUrl: ev.detail === 1,\n                    });\n                }\n            }\n        });\n\n        this._onSelectionChange = this._onSelectionChange.bind(this);\n        this.odooEditor.document.addEventListener('selectionchange', this._onSelectionChange);\n\n        this.odooEditor.addEventListener('preObserverActive', () => {\n            // The setONotEditable 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 `setONotEditable` before the\n            // editor could register a mutation.\n            this._setONotEditable(this.odooEditor.editable);\n        });\n\n        return _super.apply(this, arguments).then(() => {\n            if (this.options.autohideToolbar) {\n                if (this.odooEditor.isMobile) {\n                    $(this.odooEditor.editable).before(this.toolbar.$el);\n                } else {\n                    $(document.body).append(this.toolbar.$el);\n                }\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        Wysiwyg.activeCollaborationChannelNames.add(channelName);\n\n        this.call('bus_service', 'onNotification', this, (notifications) => {\n            for (const { payload, type } of notifications) {\n                if (\n                    type === 'editor_collaboration' &&\n                    payload.model_name === modelName &&\n                    payload.field_name === fieldName &&\n                    payload.res_id === resId\n                ) {\n                    this._peerToPeerLoading.then(() => this.ptp.handleNotification(payload));\n                }\n            }\n        });\n        this.call('bus_service', 'addChannel', this._collaborationChannelName);\n        this.call('bus_service', 'startPolling');\n\n        // Check wether clientA is before clientB.\n        const isClientFirst = (clientA, clientB) => {\n            if (clientA.startTime === clientB.startTime) {\n                return clientA.id.localeCompare(clientB.id) < 1;\n            } else {\n                return clientA.startTime < clientB.startTime;\n            }\n        };\n        const rpcMutex = new Mutex();\n\n        this._getNewPtp = () => {\n            // Wether or not the history has been sent or received at least once.\n            let historySyncAtLeastOnce = false;\n            let historySyncFinished = false;\n            let historyStepsBuffer = [];\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._rpc({\n                            route: '/web_editor/bus_broadcast',\n                            params: {\n                                model_name: modelName,\n                                field_name: fieldName,\n                                res_id: resId,\n                                bus_data: rpcData,\n                            },\n                        });\n                    });\n                },\n                onRequest: {\n                    get_start_time: () => this._startCollaborationTime,\n                    get_client_name: async () => {\n                        if (!this._userName) {\n                            this._userName = (await this._rpc({\n                                model: \"res.users\",\n                                method: \"search_read\",\n                                args: [\n                                    [['id', '=', this.getSession().uid]],\n                                    ['name']\n                                ],\n                            }))[0].name;\n                        }\n                        return this._userName;\n                    },\n                    get_missing_steps: (params) => this.odooEditor.historyGetMissingSteps(params.requestPayload),\n                    get_history_from_snapshot: () => this.odooEditor.historyGetSnapshotSteps(),\n                    get_collaborative_selection: () => this.odooEditor.getCurrentCollaborativeSelection(),\n                },\n                onNotification: async ({ fromClientId, notificationName, notificationPayload }) => {\n                    switch (notificationName) {\n                        case 'ptp_remove':\n                            this.odooEditor.multiselectionRemove(notificationPayload);\n                            break;\n                        case 'rtc_signal_description':\n                            const pc = this.ptp.clientsInfos[fromClientId].peerConnection;\n                            if (this._couldBeDisconnected && this._navigatorCheckOnlineWorking && (!pc || pc.connectionState === 'closed')) {\n                                this._signalOnline();\n                            }\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.ptp.requestClient(fromClientId, 'get_start_time', undefined, { transport: 'rtc' });\n                            this.ptp.clientsInfos[fromClientId].startTime = remoteStartTime;\n                            this.ptp.requestClient(fromClientId, 'get_client_name', undefined, { transport: 'rtc' }).then((clientName) => {\n                                this.ptp.clientsInfos[fromClientId].clientName = clientName;\n                            });\n\n                            if (!historySyncAtLeastOnce) {\n                                const localClient = { id: this._currentClientId, startTime: this._startCollaborationTime };\n                                const remoteClient = { id: fromClientId, startTime: remoteStartTime };\n                                if (isClientFirst(localClient, remoteClient)) {\n                                    historySyncAtLeastOnce = true;\n                                } else {\n                                    const { steps, historyIds } = await this.ptp.requestClient(fromClientId, 'get_history_from_snapshot', undefined, { transport: 'rtc' });\n                                    // Ensure that the history hasn't been\n                                    // synced by another client before this\n                                    // `get_history_from_snapshot` finished.\n                                    if (historySyncAtLeastOnce) {\n                                        return;\n                                    }\n                                    const firstStepId = this.odooEditor.historyGetBranchIds()[0];\n                                    const staleDocument = !historyIds.includes(firstStepId);\n                                    if (staleDocument) {\n                                        return false;\n                                    }\n                                    historySyncAtLeastOnce = true;\n                                    this.odooEditor.historyResetFromSteps(steps, historyIds);\n                                    const remoteSelection = await this.ptp.requestClient(fromClientId, 'get_collaborative_selection', undefined, { transport: 'rtc' });\n                                    if (remoteSelection) {\n                                        this.odooEditor.onExternalMultiselectionUpdate(remoteSelection);\n                                    }\n                                }\n                                // In case there are steps received in the meantime, process them.\n                                if (historyStepsBuffer.length) {\n                                    this.odooEditor.onExternalHistorySteps(historyStepsBuffer);\n                                    historyStepsBuffer = [];\n                                }\n                                historySyncFinished = true;\n                            } else {\n                                const remoteSelection = await this.ptp.requestClient(fromClientId, 'get_collaborative_selection', undefined, { transport: 'rtc' });\n                                if (remoteSelection) {\n                                    this.odooEditor.onExternalMultiselectionUpdate(remoteSelection);\n                                }\n                            }\n                            break;\n                        }\n                        case 'oe_history_step':\n                            // Avoid race condition where the step is received\n                            // before the history has synced at least once.\n                            if (historySyncFinished) {\n                                this.odooEditor.onExternalHistorySteps([notificationPayload]);\n                            } else {\n                                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                            this.odooEditor.onExternalMultiselectionUpdate(selection);\n                            break;\n                        }\n                    }\n                }\n            });\n        }\n\n        this._currentClientId = this._generateClientId();\n        this._startCollaborationTime = new Date().getTime();\n\n        this._checkConnectionChange = () => {\n            this._navigatorCheckOnlineWorking = true;\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            let iceServers = await this._rpc({route: '/web_editor/get_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: _.throttle((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.ptp.requestClient(\n                    step.clientId,\n                    'get_missing_steps', {\n                        fromStepId: fromStepId,\n                        toStepId: step.id\n                    },\n                    { transport: 'rtc' }\n                );\n                // If missing steps === -1, it means that either the\n                // step.clientId has a stale document or the step.clientId has a\n                // snapshot and does not includes the step in its history.\n                if (missingSteps === -1 || !missingSteps.length) {\n                    console.warn('Editor get_missing_steps result is erroneous.');\n                    return;\n                }\n                this.ptp && this.odooEditor.onExternalHistorySteps(missingSteps.concat([step]));\n            },\n        };\n        return editorCollaborationOptions;\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        Wysiwyg.activeWysiwygs.delete(this);\n        if (this._collaborationChannelName) {\n            Wysiwyg.activeCollaborationChannelNames.delete(this._collaborationChannelName);\n        }\n\n        if (this.ptp) {\n            this.ptp.stop();\n        }\n        document.removeEventListener(\"mousemove\", this._signalOnline, true);\n        document.removeEventListener(\"keydown\", this._signalOnline, true);\n        document.removeEventListener(\"keyup\", this._signalOnline, true);\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            for (const observer of this._oNotEditableObservers.values()) {\n                observer.disconnect();\n            }\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.call('bus_service', 'deleteChannel', this._collaborationChannelName);\n                this.ptp.closeAllConnections();\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        const $wrapwrap = $('#wrapwrap');\n        if ($wrapwrap.length) {\n            $('#wrapwrap')[0].removeEventListener('scroll', this.odooEditor.multiselectionRefresh, { passive: true });\n        }\n        $(this.$root).off('mousedown');\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        document.removeEventListener('scroll', this._onScroll, true);\n        this._super();\n    },\n    /**\n     * @override\n     */\n    renderElement: function () {\n        this.$editable = this.options.editable || $('<div class=\"note-editable\">');\n        this.$root = this.$editable;\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 && !device.isMobile) {\n            const $wrapper = $('<div class=\"o_wysiwyg_wrapper odoo-editor\">');\n            this.$root = $wrapper;\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            this._replaceElement(this.$editable);\n        }\n    },\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n    /**\n     * Return the editable area.\n     *\n     * @returns {jQuery}\n     */\n    getEditable: function () {\n        return this.$editable;\n    },\n    /**\n     * Return true if the content has changed.\n     *\n     * @returns {Boolean}\n     */\n    isDirty: function () {\n        return this._initialValue !== (this.getValue() || this.$editable.val());\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: function (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-original-title=\"\"]').removeAttr('data-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-original-title');\n        this.odooEditor.cleanForSave($editable[0]);\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: function () {\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     * Save the content for the normal mode or the translation mode.\n     */\n    saveContent: async function (reload = true) {\n        await this.saveToServer(reload);\n    },\n    /**\n     * Reset the history.\n     */\n    historyReset: function () {\n        this.odooEditor.historyReset();\n    },\n    /**\n     * Save the content to the server for the normal mode.\n     */\n    saveToServer: async function (reload = true) {\n        const defs = [];\n        if (!this.__edition_will_stopped_already_done) {\n            // TODO remove in master\n            this.trigger_up('edition_will_stopped');\n        }\n        this.trigger_up('ready_to_save', {defs: defs});\n        await Promise.all(defs);\n\n        await this.cleanForSave();\n\n        const editables = this.options.getContentEditableAreas();\n        await this.saveModifiedImages(editables.length ? $(editables) : this.$editable);\n        await this._saveViewBlocks();\n\n        this.trigger_up('edition_was_stopped');\n        window.removeEventListener('beforeunload', this._onBeforeUnload);\n        if (reload) {\n            window.location.reload();\n        }\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: function (reload) {\n        var self = this;\n        return new Promise((resolve, reject) => {\n            if (!this.odooEditor.historySize().length) {\n                resolve();\n            } else {\n                var confirm = Dialog.confirm(this, _t(\"If you discard the current edits, all unsaved changes will be lost. You can cancel to return to edit mode.\"), {\n                    confirm_callback: resolve,\n                });\n                confirm.on('closed', self, reject);\n            }\n        }).then(function () {\n            if (reload !== false) {\n                window.onbeforeunload = null;\n                return self._reload();\n            }\n        });\n    },\n    /**\n     * Create/Update cropped attachments.\n     *\n     * @param {jQuery} $editable\n     * @returns {Promise}\n     */\n    saveModifiedImages: function ($editable = this.$editable) {\n        const defs = _.map($editable, async editableEl => {\n            const {oeModel: resModel, oeId: resId} = editableEl.dataset;\n            const proms = [...editableEl.querySelectorAll('.o_modified_image_to_save')].map(async el => {\n                const isBackground = !el.matches('img');\n                el.classList.remove('o_modified_image_to_save');\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                const newAttachmentSrc = await this._rpc({\n                    route: `/web_editor/modify_image/${encodeURIComponent(el.dataset.originalId)}`,\n                    params: {\n                        res_model: resModel,\n                        res_id: parseInt(resId),\n                        data: (isBackground ? el.dataset.bgSrc : el.getAttribute('src')).split(',')[1],\n                        mimetype: el.dataset.mimetype,\n                        name: (el.dataset.fileName ? el.dataset.fileName : null),\n                    },\n                });\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                }\n            });\n            return Promise.all(proms);\n        });\n        return Promise.all(defs);\n    },\n    /**\n     * @param {String} value\n     * @returns {String}\n     */\n    setValue: function (value) {\n        this.$editable.html(value);\n        this.odooEditor.sanitize();\n        this.odooEditor.historyStep(true);\n    },\n    /**\n     * Undo one step of change in the editor.\n     */\n    undo: function () {\n        this.odooEditor.historyUndo();\n    },\n    /**\n     * Redo one step of change in the editor.\n     */\n    redo: function () {\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: function () {\n        if (this.odooEditor && !this.odooEditor.historyResetLatestComputedSelection()) {\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    isSelectionInEditable: function () {\n        return this.odooEditor.isSelectionInEditable();\n    },\n    cleanForSave: async function () {\n        this.odooEditor.clean();\n        this.$editable.find('.oe_edited_link').removeClass('oe_edited_link');\n        const historyIds = this.odooEditor.historyGetBranchIds().join(',');\n        if (this.options.collaborative) {\n            this.odooEditor.editable.children[0].setAttribute('data-last-history-steps', historyIds);\n        }\n        if (this.snippetsMenu) {\n            await this.snippetsMenu.cleanForSave();\n        }\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: function () {\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-id]';\n            const $odooFields = this.$editable.find(odooFieldSelector);\n            this.odooFieldObservers = [];\n\n            $odooFields.each((i, field) => {\n                const observer = new MutationObserver((mutations) => {\n                    mutations = this.odooEditor.filterMutationRecords(mutations);\n                    if (!mutations.length) {\n                        return;\n                    }\n                    let $node = $(field);\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-id')) {\n                        $nodes = $nodes.filter('[data-oe-translation-id=\"' + $node.data('oe-translation-id') + '\"]');\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                    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                    const html = $node.html();\n                    for (const node of $nodes) {\n                        if (node.innerHTML !== html) {\n                            node.innerHTML = html;\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: function () {\n        for (let observerData of this.odooFieldObservers) {\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 instanceof HTMLElement\n                && (targetEl.tagName === 'IMG' || targetEl.querySelectorAll('img').length === 1)) {\n            core.bus.trigger('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.noFocusUrl=false] Disable the automatic focusing of the URL field.\n     */\n    toggleLinkTools(options = {}) {\n        const linkEl = getInSelection(this.odooEditor.document, 'a');\n        if (linkEl && (!linkEl.matches(this.customizableLinksSelector) || !linkEl.isContentEditable)) {\n            return;\n        }\n        if (this.snippetsMenu && !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.noFocusUrl) {\n                    // Wait for the editor panel to be fully updated.\n                    this.snippetsMenu._mutex.exec(() => {\n                        // This is needed to focus the URL input when clicking\n                        // on the \"Edit link\" of the popover.\n                        core.bus.trigger('activate_image_link_tool');\n                    });\n                }\n                return;\n            }\n            if (options.forceOpen || !this.linkTools) {\n                const $btn = this.toolbar.$el.find('#create-link');\n                if (!this.linkTools || ![options.link, ...wysiwygUtils.ancestors(options.link)].includes(this.linkTools.$link[0])) {\n                    const { link } = Link.getOrCreateLink({\n                        containerNode: this.odooEditor.editable,\n                        startNode: options.link || this.lastMediaClicked,\n                    });\n                    if (!link) {\n                        return\n                    }\n                    const linkToolsData = Object.assign({}, this.options.defaultDataForLinkTools);\n                    this.linkTools = new weWidgets.LinkTools(this, {wysiwyg: this, noFocusUrl: options.noFocusUrl}, this.odooEditor.editable, linkToolsData, $btn, link );\n                }\n                this.linkTools.noFocusUrl = options.noFocusUrl;\n                const _onClick = ev => {\n                    if (\n                        !ev.target.closest('#create-link') &&\n                        (!ev.target.closest('.oe-toolbar') || !ev.target.closest('we-customizeblock-option')) &&\n                        !ev.target.closest('.ui-autocomplete') &&\n                        (!this.linkTools || ![ev.target, ...wysiwygUtils.ancestors(ev.target)].includes(this.linkTools.$link[0]))\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', _onClick, true);\n                    }\n                };\n                this.odooEditor.document.addEventListener('click', _onClick, true);\n                if (!this.linkTools.$el) {\n                    this.linkTools.appendTo(this.toolbar.$el);\n                }\n            } else {\n                this.destroyLinkTools();\n            }\n        } else {\n            const historyStepIndex = this.odooEditor.historySize() - 1;\n            this.odooEditor.historyPauseSteps();\n            let { link } = Link.getOrCreateLink({\n                containerNode: this.odooEditor.editable,\n                startNode: options.link,\n            });\n            if (!link) {\n                return\n            }\n            const linkDialog = new weWidgets.LinkDialog(this, {\n                forceNewWindow: this.options.linkForceNewWindow,\n                wysiwyg: this,\n                focusField: link.innerHTML ? 'url' : '',\n            }, this.$editable[0], {\n                needLabel: true\n            }, undefined, link);\n            linkDialog.open();\n            this.odooEditor.document.getSelection().collapseToEnd(); //To hide toolbar when link dialog is open.\n            linkDialog.on('save', this, data => {\n                if (!data) {\n                    return;\n                }\n                const linkWidget = linkDialog.linkWidget;\n                getDeepRange(this.$editable[0], {range: data.range, select: true});\n                if (this.options.userGeneratedContent) {\n                    data.rel = 'ugc';\n                }\n                linkWidget.applyLinkToDom(data);\n                this.odooEditor.historyUnpauseSteps();\n                this.odooEditor.historyStep();\n                link = linkWidget.$link[0];\n                setSelection(link, 0, link, link.childNodes.length, false);\n                // Focus the link after the dialog element is removed because\n                // if the dialog element is still in the DOM at the time of\n                // doing link.focus(), because there is the attribute tabindex\n                // on the dialog element, the focus cannot occurs.\n                // Using a microtask to set the focus is hackish and might break\n                // if another microtask wich focus an elemen in the dom occurs\n                // at the same time (but this case seems unlikely).\n                Promise.resolve().then(() => link.focus());\n            });\n            linkDialog.on('closed', this, function () {\n                this.odooEditor.historyUnpauseSteps();\n                // If the linkDialog content has been saved\n                // the previous selection in not relevant anymore.\n                if (linkDialog.destroyAction !== 'save') {\n                    this.odooEditor.historyRevertUntil(historyStepIndex)\n                }\n            });\n        }\n    },\n    /**\n     * Removes the current Link.\n     */\n    removeLink() {\n        if (this.snippetsMenu && wysiwygUtils.isImg(this.lastElement)) {\n            this.snippetsMenu._mutex.exec(() => {\n                // Wait for the editor panel to be fully updated.\n                core.bus.trigger('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    destroyLinkTools() {\n        if (this.linkTools) {\n            const selection = this.odooEditor.document.getSelection();\n            const link = this.linkTools.$link[0];\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 (this.linkTools.shouldUnlink()) {\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                    anchorNode = link;\n                    focusNode = link;\n                }\n                if (!focusOffset) {\n                    focusOffset = focusNode.childNodes.length || focusNode.length;\n                }\n            }\n            this.linkTools.destroy();\n            if (anchorNode) {\n                setSelection(anchorNode, anchorOffset, focusNode, focusOffset, false);\n            }\n            this.linkTools = undefined;\n        }\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     */\n    openMediaDialog(params = {}) {\n        const sel = this.odooEditor.document.getSelection();\n        const fontawesomeIcon = getInSelection(this.odooEditor.document, '.fa');\n        if (fontawesomeIcon && sel.toString().trim() === \"\") {\n            params.node = $(fontawesomeIcon);\n            // save layouting classes from icons to not break the page if you edit an icon\n            params.htmlClass = [...fontawesomeIcon.classList].filter((className) => {\n                return !className.startsWith('fa') || faZoomClassRegex.test(className);\n            }).join(' ');\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 $node = $(params.node);\n        // We need to keep track of FA icon or video because media.js will _clear those classes\n        const wasFontAwesome = $node.hasClass('fa');\n        const wasImageOrVideo = wysiwygUtils.isImg($node[0]);\n        const $editable = $(OdooEditorLib.closestElement(range.startContainer, '.o_editable'));\n        const model = $editable.data('oe-model');\n        const field = $editable.data('oe-field');\n        const type = $editable.data('oe-type');\n\n        const mediaParams = Object.assign({\n            res_model: model,\n            res_id: $editable.data('oe-id'),\n            domain: $editable.data('oe-media-domain'),\n            useMediaLibrary: field && (model === 'ir.ui.view' && field === 'arch' || type === 'html'),\n        }, this.options.mediaModalParams, params);\n        const mediaDialog = new weWidgets.MediaDialog(this, mediaParams, $node);\n        mediaDialog.open();\n\n        mediaDialog.on('save', this, function (element) {\n            if (!element) {\n                return;\n            }\n            // restore saved html classes\n            if (params.htmlClass) {\n                element.className += \" \" + params.htmlClass;\n            }\n            restoreSelection();\n            if (wasImageOrVideo || wasFontAwesome) {\n                $node.replaceWith(element);\n                this.odooEditor.unbreakableStepUnactive();\n                this.odooEditor.historyStep();\n            } else if (element) {\n                this.odooEditor.execCommand('insertHTML', element.outerHTML);\n            }\n        });\n        mediaDialog.on('closed', this, function () {\n            // if the mediaDialog content has been saved\n            // the previous selection in not relevant anymore\n            if (mediaDialog.destroyAction !== 'save') {\n                restoreSelection();\n            }\n        });\n    },\n    getInSelection(selector) {\n        return getInSelection(this.odooEditor.document, selector);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Returns an instance of the snippets menu.\n     *\n     * @param {Object} [options]\n     * @returns {widget}\n     */\n    _createSnippetsMenuInstance: function (options={}) {\n        return new snippetsEditor.SnippetsMenu(this, Object.assign({\n            wysiwyg: this,\n            selectorEditableArea: '.o_editable',\n        }, options));\n    },\n    _configureToolbar: function (options) {\n        const $toolbar = this.toolbar.$el;\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.target.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                    new weWidgets.AltDialog(this, {}, this.lastMediaClicked).open();\n                    break;\n            }\n        };\n        if (!this.options.snippets) {\n            $toolbar.find('#justify, #table, #media-insert').remove();\n        }\n        $toolbar.find('#media-insert, #media-replace, #media-description').click(openTools);\n        $toolbar.find('#create-link').click(openTools);\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-left', 'float-right');\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(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            new weWidgets.ImageCropWidget(this, this.lastMediaClicked).appendTo(this.odooEditor.document.body);\n            this.odooEditor.toolbarHide();\n            $(this.lastMediaClicked).on('image_cropper_destroyed', () => this.odooEditor.toolbarShow());\n        });\n        $toolbar.find('#image-transform').click(e => {\n            if (!this.lastMediaClicked) {\n                return;\n            }\n            const $image = $(this.lastMediaClicked);\n            if ($image.data('transfo-destroy')) {\n                $image.removeData('transfo-destroy');\n                return;\n            }\n            $image.transfo({document: this.odooEditor.document});\n            const mouseup = () => {\n                $('#image-transform').toggleClass('active', $image.is('[style*=\"transform\"]'));\n            };\n            $(this.odooEditor.document).on('mouseup', mouseup);\n            const mousedown = mousedownEvent => {\n                if (!$(mousedownEvent.target).closest('.transfo-container').length) {\n                    $image.transfo('destroy');\n                    $(this.odooEditor.document).off('mousedown', mousedown).off('mouseup', mouseup);\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                }\n                $image.trigger('content_changed');\n            };\n            $(this.odooEditor.document).on('mousedown', mousedown);\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        const $colorpickerGroup = $toolbar.find('#colorInputButtonGroup');\n        if ($colorpickerGroup.length) {\n            this._createPalette();\n        }\n        if (!options.snippets) {\n            // Scroll event does not bubble.\n            document.addEventListener('scroll', this._onScroll, true);\n        }\n    },\n    /**\n     * @private\n     * @param {jQuery} $\n     * @param {String} eventName 'foreColor' or 'backColor'\n     * @returns {String} color\n     */\n    _getSelectedColor($, eventName) {\n        const selection = this.odooEditor.document.getSelection();\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 (eventName === \"foreColor\" && textGradient || eventName !== \"foreColor\" && !textGradient) {\n                backgroundGradient = backgroundImage;\n            }\n        }\n        return backgroundGradient || $(targetElement).css(eventName === \"foreColor\" ? 'color' : 'backgroundColor');\n    },\n    _createPalette() {\n        const $dropdownContent = this.toolbar.$el.find('#colorInputButtonGroup .colorPalette');\n        // The editor's root widget can be website or web's root widget and cannot be properly retrieved...\n        for (const elem of $dropdownContent) {\n            const eventName = elem.dataset.eventName;\n            let colorpicker = null;\n            const mutex = new concurrency.MutexedDropPrevious();\n            if (!elem.ownerDocument.defaultView) {\n                // In case the element is not in the DOM, don't do anything with it.\n                continue;\n            }\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 $ = elem.ownerDocument.defaultView.$;\n            const $dropdown = $(elem).closest('.colorpicker-group , .dropdown');\n            let manualOpening = false;\n            // Prevent dropdown closing on colorpicker click\n            $dropdown.on('hide.bs.dropdown', ev => {\n                return !(ev.clickEvent && ev.clickEvent.originalEvent && ev.clickEvent.originalEvent.__isColorpickerClick);\n            });\n            $dropdown.on('show.bs.dropdown', () => {\n                if (manualOpening) {\n                    return true;\n                }\n                mutex.exec(() => {\n                    const oldColorpicker = colorpicker;\n                    const hookEl = oldColorpicker ? oldColorpicker.el : elem;\n                    const selectedColor = this._getSelectedColor($, eventName);\n                    const selection = this.odooEditor.document.getSelection();\n                    const range = selection.rangeCount && selection.getRangeAt(0);\n                    const hadNonCollapsedSelection = range && !selection.isCollapsed;\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                    colorpicker = new ColorPaletteWidget(this, {\n                        excluded: ['transparent_grayscale'],\n                        $editable: $(this.odooEditor.editable), // Our parent is the root widget, we can't retrieve the editable section from it...\n                        selectedColor: selectedColor,\n                        selectedTab: weUtils.isColorGradient(selectedColor) ? 'gradients' : 'theme-colors',\n                        withGradients: true,\n                    });\n                    this.colorpickers[eventName] = colorpicker;\n                    colorpicker.on('custom_color_picked color_picked', null, ev => {\n                        if (hadNonCollapsedSelection) {\n                            this.odooEditor.historyResetLatestComputedSelection(true);\n                        }\n                        // Unstash the mutations now that the color is picked.\n                        this.odooEditor.historyUnstash();\n                        this._processAndApplyColor(eventName, ev.data.color);\n                        this._updateEditorUI(this.lastMediaClicked && { target: this.lastMediaClicked });\n                    });\n                    colorpicker.on('color_hover', null, ev => {\n                        if (hadNonCollapsedSelection) {\n                            this.odooEditor.historyResetLatestComputedSelection(true);\n                        }\n                        this.odooEditor.historyPauseSteps();\n                        try {\n                            this._processAndApplyColor(eventName, ev.data.color);\n                        } finally {\n                            this.odooEditor.historyUnpauseSteps();\n                        }\n                    });\n                    colorpicker.on('color_leave', null, ev => {\n                        this.odooEditor.historyRevertCurrentStep();\n                    });\n                    colorpicker.on('enter_key_color_colorpicker', null, () => {\n                        $dropdown.children('.dropdown-toggle').dropdown('hide');\n                    });\n                    return colorpicker.replace(hookEl).then(() => {\n                        if (oldColorpicker) {\n                            oldColorpicker.destroy();\n                        }\n                        manualOpening = true;\n                        $dropdown.children('.dropdown-toggle').dropdown('show');\n                        const $colorpicker = $dropdown.find('.colorpicker');\n                        const colorpickerHeight = $colorpicker.outerHeight();\n                        const toolbarContainerTop = dom.closestScrollable(this.toolbar.el).getBoundingClientRect().top;\n                        const toolbarColorButtonTop = this.toolbar.el.querySelector('#colorInputButtonGroup').getBoundingClientRect().top;\n                        $dropdown[0].classList.toggle('dropup', colorpickerHeight + toolbarContainerTop <= toolbarColorButtonTop);\n                        manualOpening = false;\n                    });\n                });\n                return false;\n            });\n        }\n    },\n    _processAndApplyColor: function (eventName, color) {\n        if (color && (!ColorpickerWidget.isCSSColor(color) && !weUtils.isColorGradient(color))) {\n            color = (eventName === \"foreColor\" ? 'text-' : 'bg-') + color;\n        }\n        const fonts = this.odooEditor.execCommand('applyColor', color, eventName === 'foreColor' ? 'color' : 'backgroundColor', this.lastMediaClicked);\n\n        if (!this.lastMediaClicked && fonts && fonts.length) {\n            // Ensure the selection in the fonts tags, otherwise an undetermined\n            // race condition could generate a wrong selection later.\n            const first = fonts[0];\n            const last = fonts[fonts.length - 1];\n\n            const sel = this.odooEditor.document.getSelection();\n            sel.removeAllRanges();\n            const range = new Range();\n            range.setStart(first, 0);\n            range.setEnd(...endPos(last));\n            sel.addRange(range);\n        }\n\n        const hexColor = this._colorToHex(color);\n        this.odooEditor.updateColorpickerLabels({\n            [eventName === 'foreColor' ? 'foreColor' : 'hiliteColor']: hexColor,\n        });\n    },\n    _colorToHex: function (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: function (e) {\n        const options = this._editorOptions();\n        // Open the link tool when CTRL+K is pressed.\n        if (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: function (e) {\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.toolbar.$el.find('#mediaParagraphDropdownButton').attr('id', 'paragraphDropdownButton');\n        // Hide the create-link button if the selection spans several blocks.\n        const selection = this.odooEditor.document.getSelection();\n        const range = selection && selection.rangeCount && selection.getRangeAt(0);\n        const $rangeContainer = range && $(range.commonAncestorContainer);\n        const spansBlocks = range && !!$rangeContainer.contents().filter((i, node) => isBlock(node)).length;\n        this.toolbar.$el.find('#create-link').toggleClass('d-none', !range || spansBlocks);\n        // Only show the media tools in the toolbar if the current selected\n        // snippet is a media.\n        const isInMedia = $target.is(mediaSelector) && e.target &&\n            (e.target.isContentEditable || (e.target.parentElement && e.target.parentElement.isContentEditable));\n        this.toolbar.$el.find([\n            '#image-shape',\n            '#image-width',\n            '#image-padding',\n            '#image-edit',\n            '#media-replace',\n        ].join(',')).toggleClass('d-none', !isInMedia);\n        // The image replace button is in the image options when the sidebar\n        // exists.\n        if (this.snippetsMenu && $target.is('img')) {\n            this.toolbar.$el.find('#media-replace').toggleClass('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        this.toolbar.$el.find([\n            '#image-transform',\n            '#image-crop',\n            '#media-description',\n        ].join(',')).toggleClass('d-none', !$target.is('img'));\n        this.lastMediaClicked = isInMedia && e.target;\n        this.lastElement = $target[0];\n        // Hide the irrelevant text buttons for media.\n        this.toolbar.$el.find([\n            '#style',\n            '#decoration',\n            '#font-size',\n            '#justifyFull',\n            '#list',\n            '#colorInputButtonGroup',\n            '#table',\n            '#create-link',\n            '#media-insert', // \"Insert media\" should be replaced with \"Replace media\".\n        ].join(',')).toggleClass('d-none', isInMedia);\n        // Some icons are relevant for icons, that aren't for other media.\n        this.toolbar.$el.find('#colorInputButtonGroup, #create-link').toggleClass('d-none', isInMedia && !$target.is('.fa'));\n        this.toolbar.$el.find('.only_fa').toggleClass('d-none', !$target.is('.fa'));\n        // Toggle the toolbar arrow.\n        this.toolbar.$el.toggleClass('noarrow', isInMedia);\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            // Always hide the unlink button on media.\n            this.toolbar.$el.find('#unlink').toggleClass('d-none', true);\n            // Toggle the 'active' class on the active image tool buttons.\n            for (const button of this.toolbar.$el.find('#image-shape div, #fa-spin')) {\n                button.classList.toggle('active', $(e.target).hasClass(button.id));\n            }\n            for (const button of this.toolbar.$el.find('#image-width div')) {\n                button.classList.toggle('active', e.target.style.width === button.id);\n            }\n            this._updateMediaJustifyButton();\n            this._updateFaResizeButtons();\n        }\n        const link = getInSelection(this.odooEditor.document, this.customizableLinksSelector);\n        if (isInMedia || (link && link.isContentEditable)) {\n            // Handle the media/link's tooltip.\n            this.showTooltip = true;\n            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                this.odooEditor.observerUnactive();\n                $target.tooltip({title: _t('Double-click to edit'), trigger: 'manual', container: 'body'}).tooltip('show');\n                this.odooEditor.observerActive();\n                setTimeout(() => $target.tooltip('dispose'), 800);\n            }, 400);\n        }\n        // Update color of already opened colorpickers.\n        setTimeout(() => {\n            for (let eventName in this.colorpickers) {\n                const selectedColor = this._getSelectedColor($, eventName);\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.colorpickers[eventName].setSelectedColor(null, selectedColor, false);\n                }\n            }\n        }, 0);\n    },\n    _updateMediaJustifyButton: function (commandState) {\n        if (!this.lastMediaClicked) {\n            return;\n        }\n        const $paragraphDropdownButton = this.toolbar.$el.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-left', 'justifyLeft'],\n                ['mx-auto', 'justifyCenter'],\n                ['float-right', 'justifyRight'],\n            ];\n            commandState = (justifyMapping.find(pair => (\n                this.lastMediaClicked.classList.contains(pair[0]))\n            ) || [])[1];\n            resetAlignment = !commandState;\n        }\n        const $buttons = this.toolbar.$el.find('#justify div.btn');\n        let newClass;\n        if (commandState) {\n            const direction = commandState.replace('justify', '').toLowerCase();\n            newClass = `fa-align-${direction === 'full' ? 'justify' : direction}`;\n            resetAlignment = !['float-left', 'mx-auto', 'float-right'].some(className => (\n                this.lastMediaClicked.classList.contains(className)\n            ));\n        }\n        for (const button of $buttons) {\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: function () {\n        if (!this.lastMediaClicked) {\n            return;\n        }\n        const $buttons = this.toolbar.$el.find('#fa-resize div');\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 $buttons) {\n            button.classList.toggle('active', button.dataset.value === value);\n        }\n    },\n    _editorOptions: function () {\n        return Object.assign({}, this.defaultOptions, this.options);\n    },\n    _insertSnippetMenu: function () {\n        return this.snippetsMenu.insertBefore(this.$el);\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: function ($el, context, withLang = true) {\n        if ($el.data('oe-translation-id')) {\n            return this._rpc({\n                model: 'ir.translation',\n                method: 'save_html',\n                args: [\n                    [+$el.data('oe-translation-id')],\n                    this._getEscapedElement($el).html()\n                ],\n                context: context,\n            });\n        } else {\n            var viewID = $el.data('oe-id');\n            if (!viewID) {\n                return Promise.resolve();\n            }\n\n            return this._rpc({\n                model: 'ir.ui.view',\n                method: 'save',\n                args: [\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                ],\n                context: context,\n            }, withLang ? undefined : {\n                noContextKeys: 'lang',\n            });\n        }\n    },\n    _getCommands: function () {\n        const options = this._editorOptions();\n        const commands = [\n            {\n                groupName: _t('Basic blocks'),\n                title: _t('Quote'),\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                groupName: _t('Basic blocks'),\n                title: _t('Code'),\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                groupName: _t('Navigation'),\n                title: _t('Link'),\n                description: _t('Add a link.'),\n                fontawesome: 'fa-link',\n                callback: () => {\n                    this.toggleLinkTools({forceDialog: true});\n                },\n            },\n            {\n                groupName: _t('Navigation'),\n                title: _t('Button'),\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                        $(\".o_link_dialog .link-style[value=primary]\").click();\n                    }, 150);\n                },\n            },\n        ];\n        if (options.isInternalUser) {\n            commands.push({\n                groupName: _t('Medias'),\n                title: _t('Image'),\n                description: _t('Insert an image.'),\n                fontawesome: 'fa-file-image-o',\n                callback: () => {\n                    this.openMediaDialog();\n                },\n            });\n        }\n        if (options.allowCommandVideo) {\n            commands.push({\n                groupName: _t('Medias'),\n                title: _t('Video'),\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 (options.powerboxCommands) {\n            commands.push(...options.powerboxCommands);\n        }\n        return commands;\n    },\n\n    /**\n     * Returns the editable areas on the page.\n     *\n     * @returns {jQuery}\n     */\n    editable: function () {\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 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     * @return {Promise} rejected if the save cannot be done\n     */\n    _saveViewBlocks: function (context) {\n\n        const $allBlocks = $((this.options || {}).savableSelector).filter('.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        $('.o_editable')\n            .removeClass('o_editable o_is_inline_editable o_editable_date_field_linked o_editable_date_field_format_changed');\n\n        const defs = _.map($allBlocks, (el) => {\n            const $el = $(el);\n\n            $el.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 this.saving_mutex.exec(() => {\n                let saveElement = '_saveElement';\n                if (this.options.enableTranslation) {\n                    saveElement = '_saveTranslationElement';\n                }\n                return this[saveElement]($el, context || weContext.get())\n                .then(function () {\n                    $el.removeClass('o_dirty');\n                }).guardedCatch(function (response) {\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                    $el.addClass('o_dirty o_editable oe_carlos_danger ' + id);\n                    $('.o_editable.' + id)\n                        .removeClass(id)\n                        .popover({\n                            trigger: 'hover',\n                            content: response.message.data.message || '',\n                            placement: 'auto',\n                        })\n                        .popover('show');\n                });\n            });\n        });\n        return Promise.all(defs).then(function () {\n            window.onbeforeunload = null;\n        }).guardedCatch((failed) => {\n            // If there were errors, re-enable edition\n            this.cancel(false);\n        });\n    },\n    // TODO unused => remove or reuse as it should be\n    _attachTooltips: function () {\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: function ($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     * @param {boolean} [withLang=false]\n     *        false if the lang must be omitted in the context (saving \"master\"\n     *        page element)\n     */\n    _saveElement: function ($el, context, withLang) {\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        return this._rpc({\n            model: 'ir.ui.view',\n            method: 'save',\n            args: [\n                viewID,\n                escapedHtml,\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            ],\n            context: context,\n        }, withLang ? undefined : {\n            noContextKeys: 'lang',\n        });\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: function () {\n        window.location.hash = '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    _onSelectionChange() {\n        if (this.options.autohideToolbar) {\n            const isVisible = this.linkPopover && this.linkPopover.el.offsetParent;\n            if (isVisible && !this.odooEditor.document.getSelection().isCollapsed) {\n                this.linkPopover.hide();\n            }\n        }\n    },\n    _onDocumentMousedown: function (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        if (e.target.closest('.oe-toolbar')) {\n            this._onToolbar = true;\n        } else {\n            if (this._pendingBlur && !e.target.closest('.o_wysiwyg_wrapper')) {\n                this.trigger_up('wysiwyg_blur');\n                this._pendingBlur = false;\n            }\n            this._onToolbar = false;\n        }\n    },\n    _onBlur: function () {\n        if (this._onToolbar) {\n            this._pendingBlur = true;\n        } else {\n            this.trigger_up('wysiwyg_blur');\n        }\n    },\n    _onScroll: function(ev) {\n        if (ev.target.contains(this.$editable[0])) {\n            this.scrollContainer = ev.target;\n            this.odooEditor.updateToolbarPosition();\n        }\n    },\n    _signalOffline: function () {\n        if (!this._isOnline) {\n            return;\n        }\n        this._isOnline = false;\n\n        this.preSavePromise = new Promise((resolve, reject) => {\n            this.preSavePromiseResolve = resolve;\n            this.preSavePromiseReject = reject;\n        });\n    },\n    _signalOnline: async function () {\n        clearTimeout(this._offlineTimeout);\n        this._offlineTimeout = undefined;\n\n        if (this._isOnline || !this.preSavePromise || !navigator.onLine) {\n            return;\n        }\n        this._isOnline = true;\n\n        if (this._removeSignalDisconnectCallback) {\n            this._removeSignalDisconnectCallback();\n        }\n        const resetPreSavePromise = () => {\n            this.preSavePromise = undefined;\n            this.preSavePromiseResolve = undefined;\n            this.preSavePromiseReject = undefined;\n        }\n        try {\n            const serverContent = await this._ensureCommonHistory();\n            if (serverContent) {\n                const $dialogContent = $(QWeb.render('web_editor.collaboration-reset-dialog'));\n                $dialogContent.append($(this.odooEditor.editable).clone());\n                const dialog = new Dialog(this, {\n                    title: _t(\"Content conflict\"),\n                    $content: $dialogContent,\n                    size: 'medium',\n                });\n                dialog.open({shouldFocusButtons:true});\n\n                this._resetEditor(serverContent);\n            }\n            this.preSavePromiseResolve();\n            resetPreSavePromise();\n        } catch (e) {\n            this.preSavePromiseReject && this.preSavePromiseReject(e);\n            resetPreSavePromise();\n        }\n    },\n    _getInitialHistoryId: function (value) {\n        const matchId = value.match(/data-last-history-steps=\"([0-9,]*?)\"/);\n        return matchId && matchId[1];\n    },\n    /**\n     * When the collaboration is active, ensure that we do not try to save with\n     * a different history branch to the database. If the history is different,\n     * return the database html content.\n     *\n     * See `_historyIds` in `historyReset` in OdooEditor.\n     *\n     * @return {string} The database html content if the history is different.\n     */\n    async _ensureCommonHistory() {\n        if (!this.ptp) return;\n        const historyIds = this.odooEditor.historyGetBranchIds();\n        return this._rpc({\n            route: '/web_editor/ensure_common_history',\n            params: {\n                history_ids: historyIds,\n                model_name: this.options.collaborationChannel.collaborationModelName,\n                field_name: this.options.collaborationChannel.collaborationFieldName,\n                res_id: this.options.collaborationChannel.collaborationResId,\n            },\n        });\n    },\n    _generateClientId: function () {\n        // No need for secure random number.\n        return Math.floor(Math.random() * Math.pow(2, 52)).toString();\n    },\n    _resetEditor: function (value) {\n        if (!this.ptp) {\n            return;\n        }\n        this.ptp.stop();\n        this._currentClientId = this._generateClientId();\n        this._startCollaborationTime = new Date().getTime();\n        this.ptp = this._getNewPtp();\n        this.odooEditor.collaborationSetClientId(this._currentClientId);\n        this.setValue(value);\n        this.odooEditor.historyReset();\n        const initialHistoryId = value && this._getInitialHistoryId(value);\n        if (initialHistoryId) {\n            this.odooEditor.historySetInitialId(initialHistoryId);\n        }\n        this.ptp.notifyAllClients('ptp_join');\n    },\n    /**\n     * Set contenteditable=false for all `.o_not_editable` found within node if\n     * node is an element.\n     *\n     * For all `.o_not_editable` element found, the attribute contenteditable\n     * will be removed if the class is removed.\n     *\n     * @param {Node} node\n     */\n    _setONotEditable: function (node) {\n        const nodes = (node && node.querySelectorAll && node.querySelectorAll('.o_not_editable:not([contenteditable=false])')) || [];\n        for (const node of nodes) {\n            node.setAttribute('contenteditable', false);\n            let observer = this._oNotEditableObservers.get(node);\n            if (!observer) {\n                observer = new MutationObserver((records) => {\n                    for (const record of records) {\n                        if (record.type === 'attributes' && record.attributeName === 'class') {\n                            // Remove contenteditable=false on nodes that were\n                            // previsouly o_not_editable but are no longer\n                            // o_not_editable.\n                            if (!node.classList.contains('o_not_editable')) {\n                                this.odooEditor.observerUnactive('_setONotEditable');\n                                node.removeAttribute('contenteditable');\n                                this.odooEditor.observerActive('_setONotEditable');\n                                observer.disconnect();\n                                this._oNotEditableObservers.delete(node);\n                            }\n                        }\n                    }\n                });\n                this._oNotEditableObservers.set(node, observer);\n                observer.observe(node, {\n                    attributes: true,\n                });\n            }\n        }\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};\nreturn Wysiwyg;\n});\nodoo.define('web_editor.widget', function (require) {\n'use strict';\n    return {\n        Dialog: require('wysiwyg.widgets.Dialog'),\n        MediaDialog: require('wysiwyg.widgets.MediaDialog'),\n        LinkDialog: require('wysiwyg.widgets.LinkDialog'),\n    };\n});\n", "odoo.define('web_editor.wysiwyg.iframe', function (require) {\n'use strict';\n\nvar Wysiwyg = require('web_editor.wysiwyg');\nvar ajax = require('web.ajax');\nvar core = require('web.core');\nvar config = require('web.config');\n\nvar qweb = core.qweb;\nvar promiseCommon;\nvar promiseWysiwyg;\n\n\n/**\n * Add option (inIframe) to load Wysiwyg in an iframe.\n **/\nWysiwyg.include({\n    /**\n     * Add options to load Wysiwyg in an iframe.\n     *\n     * @override\n     * @param {boolean} options.inIframe\n     **/\n    init: function (parent, options) {\n        this._super.apply(this, arguments);\n        if (this.options.inIframe) {\n            this._onUpdateIframeId = 'onLoad_' + this.id;\n        }\n    },\n    /**\n     * Load assets to inject into iframe.\n     *\n     * @override\n     **/\n    willStart: async function () {\n        if (!this.options.inIframe) {\n            return this._super();\n        }\n\n        var defAsset;\n        if (this.options.iframeCssAssets) {\n            defAsset = ajax.loadAsset(this.options.iframeCssAssets);\n        } else {\n            defAsset = Promise.resolve({\n                cssLibs: [],\n                cssContents: []\n            });\n        }\n\n        promiseWysiwyg = promiseWysiwyg || ajax.loadAsset('web_editor.wysiwyg_iframe_editor_assets');\n        this.defAsset = Promise.all([promiseWysiwyg, defAsset]);\n\n        this.$target = this.$el;\n        const _super = this._super.bind(this);\n\n        await this.defAsset;\n        await _super();\n    },\n\n    /**\n     * @override\n     **/\n    start: async function () {\n        const _super = this._super.bind(this);\n        if (!this.options.inIframe) {\n            return _super();\n        } else {\n            await this._loadIframe();\n            return _super();\n        }\n    },\n\n    /**\n     * @override\n     **/\n    destroy: function() {\n        if (this.options.inIframe && this.options.document) {\n            this.options.document.removeEventListener('scroll', this._onScroll, true);\n        }\n        this._super.apply(this, arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     **/\n    _editorOptions: function () {\n        let options = this._super.apply(this, arguments);\n        options.getContextFromParentRect = () => {\n            return this.$iframe && this.$iframe.length ? this.$iframe[0].getBoundingClientRect() : { top: 0, left: 0 };\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: function () {\n        var self = this;\n        this.$iframe = $('<iframe class=\"wysiwyg_iframe\">').css({\n            'min-height': '55vh',\n            width: '100%'\n        });\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\", config.device.isMobile);\n                const $utilsZone = $('<div class=\"iframe-utils-zone\">');\n                self.$utilsZone = $utilsZone;\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                $iframeTarget.append($utilsZone);\n                $iframeWrapper.append(self.$editable);\n\n                self.options.toolbarHandler = $('#web_editor-top-edit', self.$iframe[0].contentWindow.document);\n                $iframeTarget.on('click', '.o_fullscreen_btn', function () {\n                    $(\"body\").toggleClass(\"o_field_widgetTextHtml_fullscreen\");\n                    var full = $(\"body\").hasClass(\"o_field_widgetTextHtml_fullscreen\");\n                    self.$iframe.parents().toggleClass('o_form_fullscreen_ancestor', full);\n                    $(window).trigger(\"resize\"); // induce a resize() call and let other backend elements know (the navbar extra items management relies on this)\n                });\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                var iframeContent = qweb.render('wysiwyg.iframeContent', {\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>${iframeContent}</html>`);\n            });\n            self.options.document = self.$iframe[0].contentWindow.document;\n        });\n\n        this.$iframe.insertAfter(this.$editable);\n\n        return def;\n    },\n\n    _insertSnippetMenu: function () {\n        if (this.options.inIframe) {\n            return this.snippetsMenu.appendTo(this.$utilsZone);\n        } else {\n            return this._super.apply(this, arguments);\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: function(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 this._super.apply(this, arguments);\n        }\n    },\n\n    /**\n     * @override\n     **/\n    _configureToolbar: function () {\n        this._super.apply(this, arguments);\n        if (this.options.inIframe && !this.options.snippets) {\n            this.options.document.addEventListener('scroll', this._onScroll, true);\n        }\n    },\n});\n\n});\n", "odoo.define('unsplash.api', function (require) {\n'use strict';\n\nvar Class = require('web.Class');\nvar rpc = require('web.rpc');\nvar Mixins = require('web.mixins');\nvar ServicesMixin = require('web.ServicesMixin');\n\nvar UnsplashCore = Class.extend(Mixins.EventDispatcherMixin, ServicesMixin, {\n    /**\n     * @constructor\n     */\n    init: function (parent) {\n        Mixins.EventDispatcherMixin.init.call(this, arguments);\n        this.setParent(parent);\n\n        this._cache = {};\n        this.clientId = false;\n    },\n\n    //--------------------------------------------------------------------------\n    // Public\n    //--------------------------------------------------------------------------\n\n    /**\n     * Gets unsplash images from query string.\n     *\n     * @param {String} query search terms\n     * @param {Integer} pageSize number of image to display per page\n     * @returns {Promise}\n     */\n    getImages: function (query, pageSize) {\n        var from = 0;\n        var to = pageSize;\n        var cachedData = this._cache[query];\n\n        if (cachedData && (cachedData.images.length >= to || (cachedData.totalImages !== 0 && cachedData.totalImages < to))) {\n            return Promise.resolve({ images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages });\n        }\n        return this._fetchImages(query).then(function (cachedData) {\n            return { images: cachedData.images.slice(from, to), isMaxed: to > cachedData.totalImages };\n        });\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * Fetches images from unsplash and stores it in cache\n     *\n     * @param {String} query search terms\n     * @returns {Promise}\n     * @private\n     */\n    _fetchImages: function (query) {\n        if (!this._cache[query]) {\n            this._cache[query] = {\n                images: [],\n                maxPages: 0,\n                totalImages: 0,\n                pageCached: 0\n            };\n        }\n        var cachedData = this._cache[query];\n        var payload = {\n            query: query,\n            page: cachedData.pageCached + 1,\n            per_page: 30, // max size from unsplash API\n        };\n        return this._rpc({\n            route: '/web_unsplash/fetch_images',\n            params: payload,\n        }).then(function (result) {\n            if (result.error) {\n                return Promise.reject(result.error);\n            }\n            cachedData.pageCached++;\n            cachedData.images.push.apply(cachedData.images, result.results);\n            cachedData.maxPages = result.total_pages;\n            cachedData.totalImages = result.total;\n            return cachedData;\n        });\n    },\n});\n\nreturn UnsplashCore;\n\n});\n", "odoo.define('web_unsplash.image_widgets', function (require) {\n'use strict';\n\nvar core = require('web.core');\nvar UnsplashAPI = require('unsplash.api');\nconst {UploadProgressToast} = require('@web_editor/js/wysiwyg/widgets/upload_progress_toast');\nvar widgetsMedia = require('wysiwyg.widgets.media');\nconst {_t} = require('web.core');\n\nvar unsplashAPI = null;\n\n// Prevent base class from treating unsplash images like regular attachments\nconst originalEvents = widgetsMedia.ImageWidget.prototype.events;\nconst clickHandler = originalEvents['click .o_existing_attachment_cell'];\nif (!clickHandler) {\n    throw new Error(`Couldn't find a handler for o_existing_attachment_cell clicks.\nThe unsplash image widget needs to prevent this handler from executing on unsplash attachments.`);\n}\n_.extend(originalEvents, {\n    'click .o_existing_attachment_cell:not(.o_unsplash_attachment_cell)': clickHandler,\n});\ndelete originalEvents['click .o_existing_attachment_cell'];\n\nwidgetsMedia.ImageWidget.include({\n    xmlDependencies: widgetsMedia.ImageWidget.prototype.xmlDependencies.concat(\n        ['/web_unsplash/static/src/xml/unsplash_image_widget.xml']\n    ),\n    events: _.extend({}, widgetsMedia.ImageWidget.prototype.events, {\n        'click .o_unsplash_attachment_cell[data-imgid]': '_onUnsplashImgClick',\n        'click button.save_unsplash': '_onSaveUnsplashCredentials',\n    }),\n\n    /**\n     * @override\n     */\n    init: function () {\n        this._super.apply(this, arguments);\n\n        this._unsplash = {\n            selectedImages: {},\n            isMaxed: false,\n            query: false,\n            error: false,\n            records: [],\n        };\n\n        // TODO improve this\n        //\n        // This is a `hack` to prevent the UnsplashAPI to be destroyed every\n        // time the media dialog is closed. Indeed, UnsplashAPI has a cache\n        // system to recude unsplash call, it is then better to keep its state\n        // to take advantage from it from one media dialog call to another.\n        //\n        // Unsplash API will either be (it's still being discussed):\n        //  * a service (ideally coming with an improvement to not auto load\n        //    the service)\n        //  * initialized in the website_root (trigger_up)\n        if (unsplashAPI === null) {\n            this.unsplashAPI = new UnsplashAPI(this);\n            unsplashAPI = this.unsplashAPI;\n        } else {\n            this.unsplashAPI = unsplashAPI;\n            this.unsplashAPI.setParent(this);\n        }\n    },\n    /**\n     * @override\n     */\n    destroy: function () {\n        // TODO See `hack` explained in `init`. This prevent the media dialog destroy\n        //      to destroy unsplashAPI when destroying the children\n        this.unsplashAPI.setParent(undefined);\n        this._super.apply(this, arguments);\n    },\n\n    // --------------------------------------------------------------------------\n    // Public\n    // --------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _save: async function () {\n        const _super = this._super;\n        const selectedImages = this._unsplash.selectedImages;\n        const imagesCount = Object.keys(selectedImages).length;\n        if (imagesCount) {\n            this.saved = true;\n            await this._setUpProgressToast([{\n                name: imagesCount > 1 ?\n                    _.str.sprintf(_t(\"Uploading %s '%s' images.\"), imagesCount, this._unsplash.query) :\n                    _.str.sprintf(_t(\"Uploading '%s' image.\"), this._unsplash.query),\n                size: null,\n            }]);\n            const images = await this.uploader.rpcShowProgress({\n                route: '/web_unsplash/attachment/add',\n                params: {\n                    unsplashurls: selectedImages,\n                    res_model: this.options.res_model,\n                    res_id: this.options.res_id,\n                    query: this._unsplash.query,\n                },\n            }, 0);\n            this.uploader.close(3000);\n            this.attachments.push(...images);\n            this.selectedAttachments.push(...images);\n        }\n        return _super.apply(this, arguments);\n    },\n    /**\n     * @override\n     */\n    search: async function (needle) {\n        var self = this;\n        await this._super(...arguments);\n\n        this._unsplash.query = needle;\n        if (!needle) {\n            this._unsplash.records = [];\n            return;\n        }\n\n        await this.unsplashAPI.getImages(needle, this.numberOfAttachmentsToDisplay).then(function (res) {\n            self._unsplash.isMaxed = res.isMaxed;\n            self._unsplash.records = res.images;\n            self._unsplash.error = false;\n        }, function (err) {\n            self._unsplash.error = err;\n        });\n    },\n    /**\n     * @override\n     */\n    hasContent() {\n        if (this.searchService === 'all') {\n            return this._super(...arguments) || (this.unsplashRecords && this.unsplashRecords.length);\n        } else if (this.searchService === 'unsplash') {\n            return (this.unsplashRecords && this.unsplashRecords.length);\n        }\n        return this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Private\n    //--------------------------------------------------------------------------\n\n    /**\n     * @override\n     */\n    _highlightSelected: function () {\n        this._super.apply(this, arguments);\n\n        const $select = this.$('.o_unsplash_attachment_cell[data-imgid]').filter((i, el) => {\n            return $(el).data('imgid') in this._unsplash.selectedImages;\n        }).addClass('o_we_attachment_selected');\n        return $select;\n    },\n    /**\n     * @private\n     */\n    _loadMoreImages: function (forceSearch) {\n        if (!this.$('.o_we_search').val()) {\n            return this._super(forceSearch);\n        }\n        this.numberOfAttachmentsToDisplay += 10;\n        this.search(this.$('.o_we_search').val()).then(() => this._renderThumbnails());\n    },\n    /**\n     * @override\n     */\n    _renderThumbnails: function () {\n        this._super(...arguments);\n        this.$('.unsplash_error').empty();\n        if (!['all', 'unsplash'].includes(this.searchService)) {\n            return;\n        }\n        if (this._unsplash.query && this._unsplash.error) {\n            this.$('.unsplash_error').html(\n                core.qweb.render('web_unsplash.dialog.error.content', {\n                    status: this._unsplash.error,\n                })\n            );\n            return;\n        }\n\n        if (['all', 'unsplash'].includes(this.searchService) && this._unsplash.query && !this._unsplash.isMaxed) {\n            this.$('.o_load_more').removeClass('d-none');\n            this.$('.o_load_done_msg').addClass('d-none');\n        }\n    },\n    /**\n     * @override\n     */\n    _renderExisting: function (attachments) {\n        this.unsplashRecords = this._unsplash.records.map(record => {\n            const url = new URL(record.urls.regular);\n            // In small windows, row height could get quite a bit larger than the min, so we keep some leeway.\n            url.searchParams.set('h', 2 * this.MIN_ROW_HEIGHT);\n            url.searchParams.delete('w');\n            return Object.assign({}, record, {\n                url: url.toString(),\n            });\n        });\n        return this._super(...arguments);\n    },\n    /**\n     * @override\n     */\n    _selectAttachement: function (attachment, save) {\n        if (!this.options.multiImages) {\n            this._unsplash.selectedImages = {};\n        }\n        this._super(...arguments);\n    },\n\n    //--------------------------------------------------------------------------\n    // Handlers\n    //--------------------------------------------------------------------------\n\n    /**\n     * @private\n     */\n    _onSaveUnsplashCredentials: function () {\n        var self = this;\n        var key = this.$('#accessKeyInput').val().trim();\n        var appId = this.$('#appIdInput').val().trim();\n\n        this.$('#accessKeyInput').toggleClass('is-invalid', !key);\n        this.$('#appIdInput').toggleClass('is-invalid', !appId);\n\n        if (key && appId) {\n            if (!this.$el.find('.is-invalid').length) {\n                this._rpc({\n                    route: '/web_unsplash/save_unsplash',\n                    params: {key: key, appId: appId},\n                }).then(function () {\n                    self.unsplashAPI.clientId = key;\n                    self._unsplash.error = false;\n                    self.search(self._unsplash.query).then(() => self._renderThumbnails());\n                });\n            }\n        }\n    },\n    /**\n     * @private\n     */\n    _onUnsplashImgClick: function (ev) {\n        if (this.saved) {\n            // already saved, probably a double click. Ignore.\n            return;\n        }\n        const {imgid, url, downloadUrl, description} = ev.currentTarget.dataset;\n        if (!this.options.multiImages) {\n            this._unsplash.selectedImages = {};\n            this.selectedAttachments = [];\n        }\n        if (imgid in this._unsplash.selectedImages) {\n            delete this._unsplash.selectedImages[imgid];\n        } else {\n            const _1920Url = new URL(url);\n            _1920Url.searchParams.set('w', '1920');\n            this._unsplash.selectedImages[imgid] = {url: _1920Url.href, download_url: downloadUrl, description: description};\n        }\n        this._highlightSelected();\n        if (!this.options.multiImages) {\n            this.trigger_up('save_request');\n        }\n    },\n});\n});\n"], "file": "/web/assets/4189-0d27e8f/1/web_editor.assets_wysiwyg.js", "sourceRoot": "../../../../"}