From 7594d71ca8610d5947e80f325ccb57abc23c2c76 Mon Sep 17 00:00:00 2001 From: "Arthur Detroux (ard)" Date: Tue, 10 Oct 2023 16:37:55 +0200 Subject: [PATCH] [IMP] *: remove jQueryUI draggable and droppable *: mass_mailing, test_website, web, web_editor, website, website_event, website_sale This commit removes jQueryUI draggable and droppable component to instead use our own implementation of the feature. This replaces the last use of the lib: the web_editor drag and drop. This takes advantage of the code that was already written for other features of the backend. task-3079246 closes odoo/odoo#136793 Signed-off-by: Quentin Smetz (qsm) --- .../static/tests/tours/mailing_editor.js | 4 +- .../tests/tours/mass_mailing_code_view.js | 4 +- .../static/tests/tours/custom_snippets.js | 8 +- .../tests/tours/image_upload_progress.js | 4 +- .../static/tests/tours/reset_views.js | 2 +- addons/web/static/lib/jquery.ui/jquery-ui.css | 8 +- addons/web/static/lib/jquery.ui/jquery-ui.js | 1749 +---------------- .../src/core/utils/draggable_hook_builder.js | 28 +- addons/web_editor/__manifest__.py | 2 +- .../static/src/js/editor/drag_and_drop.js | 201 ++ .../src/js/editor/smooth_scroll_on_drag.js | 420 ---- .../static/src/js/editor/snippets.editor.js | 744 ++++--- .../static/src/js/wysiwyg/MoveNodePlugin.js | 71 +- .../static/src/scss/wysiwyg_snippets.scss | 4 +- .../static/src/js/editor/snippets.editor.js | 6 +- .../website/static/src/js/tours/tour_utils.js | 6 +- .../tests/tours/conditional_visibility.js | 4 +- .../static/tests/tours/focus_blur_snippets.js | 4 +- .../website/static/tests/tours/html_editor.js | 2 +- .../tours/snippet_editor_panel_options.js | 2 +- .../tours/snippet_popup_and_animations.js | 2 +- .../tours/snippet_popup_and_scrollbar.js | 6 +- .../tests/tours/snippet_table_of_content.js | 2 + .../static/tests/tours/snippet_version.js | 4 +- .../tests/tours/snippets_all_drag_and_drop.js | 2 +- .../tests/tours/start_cloned_snippet.js | 2 +- .../static/tests/tours/website_form_editor.js | 4 +- .../static/src/js/tours/event_tour.js | 2 +- .../static/tests/tours/website_event.js | 2 +- .../static/src/js/tours/website_sale_shop.js | 2 +- ...sale_category_page_and_products_snippet.js | 2 +- 31 files changed, 683 insertions(+), 2620 deletions(-) create mode 100644 addons/web_editor/static/src/js/editor/drag_and_drop.js delete mode 100644 addons/web_editor/static/src/js/editor/smooth_scroll_on_drag.js diff --git a/addons/mass_mailing/static/tests/tours/mailing_editor.js b/addons/mass_mailing/static/tests/tours/mailing_editor.js index d9b2d292df9..b4feeb782fd 100644 --- a/addons/mass_mailing/static/tests/tours/mailing_editor.js +++ b/addons/mass_mailing/static/tests/tours/mailing_editor.js @@ -23,9 +23,9 @@ registry.category("web_tour.tours").add('mailing_editor', { run: () => {}, }, { content: 'drag the "Title" snippet from the design panel and drop it in the editor', - trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .ui-draggable-handle', + trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .oe_snippet_thumbnail', run: function (actions) { - actions.drag_and_drop('[name="body_arch"] iframe .o_editable', this.$anchor); + actions.drag_and_drop_native('[name="body_arch"] iframe .o_editable', this.$anchor); } }, { content: 'wait for the snippet menu to finish the drop process', diff --git a/addons/mass_mailing/static/tests/tours/mass_mailing_code_view.js b/addons/mass_mailing/static/tests/tours/mass_mailing_code_view.js index 45afec6e2ee..da30bc4b012 100644 --- a/addons/mass_mailing/static/tests/tours/mass_mailing_code_view.js +++ b/addons/mass_mailing/static/tests/tours/mass_mailing_code_view.js @@ -50,10 +50,10 @@ registry.category("web_tour.tours").add('mass_mailing_code_view_tour', { content: 'Verify that the dropable zone was not removed', run: () => {}, }, { - trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .ui-draggable-handle', + trigger: '[name="body_arch"] iframe #email_designer_default_body [name="Title"] .oe_snippet_thumbnail', content: 'Drag the "Title" snippet from the design panel and drop it in the editor', run: function (actions) { - actions.drag_and_drop('[name="body_arch"] iframe .o_editable', this.$anchor); + actions.drag_and_drop_native('[name="body_arch"] iframe .o_editable', this.$anchor); } }, { trigger: '[name="body_arch"] iframe .o_editable h1', diff --git a/addons/test_website/static/tests/tours/custom_snippets.js b/addons/test_website/static/tests/tours/custom_snippets.js index 4a8358119e7..1a770c46fcb 100644 --- a/addons/test_website/static/tests/tours/custom_snippets.js +++ b/addons/test_website/static/tests/tours/custom_snippets.js @@ -66,13 +66,7 @@ wTourUtils.registerWebsitePreviewTour('test_custom_snippet', { content: "confirm rename", trigger: ".oe_snippet[name='Custom Banner'] we-button.o_we_confirm_btn", }, - { - content: "drop custom snippet", - trigger: ".oe_snippet[name='Bruce Banner'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", - extra_trigger: "iframe body.editor_enable", - moveTrigger: ".oe_drop_zone", - run: "drag_and_drop iframe #wrap", - }, + wTourUtils.dragNDrop({ name: "Bruce Banner" }), { content: "ensure banner section exists", trigger: "iframe #wrap section[data-name='Banner']", diff --git a/addons/test_website/static/tests/tours/image_upload_progress.js b/addons/test_website/static/tests/tours/image_upload_progress.js index 22f156dc973..61121fc447b 100644 --- a/addons/test_website/static/tests/tours/image_upload_progress.js +++ b/addons/test_website/static/tests/tours/image_upload_progress.js @@ -48,13 +48,13 @@ const setupSteps = [{ content: "drop a snippet", trigger: "#oe_snippets .oe_snippet[name='Text - Image'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", moveTrigger: "iframe .oe_drop_zone", - run: "drag_and_drop iframe #wrap", + run: "drag_and_drop_native iframe #wrap", }, { content: "drop a snippet", trigger: "#oe_snippets .oe_snippet[name='Image Gallery'] .oe_snippet_thumbnail:not(.o_we_already_dragging)", extra_trigger: "body.editor_has_snippets", moveTrigger: ".oe_drop_zone", - run: "drag_and_drop iframe #wrap", + run: "drag_and_drop_native iframe #wrap", }]; const formatErrorMsg = "format is not supported. Try with: .gif, .jpe, .jpeg, .jpg, .png, .svg, .webp"; diff --git a/addons/test_website/static/tests/tours/reset_views.js b/addons/test_website/static/tests/tours/reset_views.js index 01fddd2a314..8adb5f8fa39 100644 --- a/addons/test_website/static/tests/tours/reset_views.js +++ b/addons/test_website/static/tests/tours/reset_views.js @@ -23,7 +23,7 @@ wTourUtils.registerWebsitePreviewTour('test_reset_page_view_complete_flow_part1' content: "drop a snippet", trigger: ".oe_snippet:has(.s_cover) .oe_snippet_thumbnail", // id starting by 'oe_structure..' will actually create an inherited view - run: "drag_and_drop iframe #oe_structure_test_website_page", + run: "drag_and_drop_native iframe #oe_structure_test_website_page", }, ...wTourUtils.clickOnSave(), // 2. Edit that COW'd view in the HTML editor to break it. diff --git a/addons/web/static/lib/jquery.ui/jquery-ui.css b/addons/web/static/lib/jquery.ui/jquery-ui.css index e1fcdc9ddcf..82ecf7f92e5 100644 --- a/addons/web/static/lib/jquery.ui/jquery-ui.css +++ b/addons/web/static/lib/jquery.ui/jquery-ui.css @@ -1,13 +1,9 @@ -/*! jQuery UI - v1.12.1 - 2023-10-04 +/*! jQuery UI - v1.12.1 - 2023-10-10 * http://jqueryui.com -* Includes: draggable.css, core.css, autocomplete.css, menu.css, theme.css +* Includes: core.css, autocomplete.css, menu.css, theme.css * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif * Copyright jQuery Foundation and other contributors; Licensed MIT */ -.ui-draggable-handle { - -ms-touch-action: none; - touch-action: none; -} /* Layout helpers ----------------------------------*/ .ui-helper-hidden { diff --git a/addons/web/static/lib/jquery.ui/jquery-ui.js b/addons/web/static/lib/jquery.ui/jquery-ui.js index 8c8a3586093..4dad240a9dc 100644 --- a/addons/web/static/lib/jquery.ui/jquery-ui.js +++ b/addons/web/static/lib/jquery.ui/jquery-ui.js @@ -1,6 +1,6 @@ -/*! jQuery UI - v1.12.1 - 2023-10-04 +/*! jQuery UI - v1.12.1 - 2023-10-10 * http://jqueryui.com -* Includes: widget.js, position.js, data.js, keycode.js, scroll-parent.js, unique-id.js, widgets/draggable.js, widgets/droppable.js, widgets/autocomplete.js, widgets/menu.js, widgets/mouse.js +* Includes: widget.js, position.js, data.js, keycode.js, unique-id.js, widgets/autocomplete.js, widgets/menu.js, widgets/mouse.js * Copyright jQuery Foundation and other contributors; Licensed MIT */ (function( factory ) { @@ -1295,41 +1295,6 @@ var keycode = $.ui.keyCode = { }; -/*! - * jQuery UI Scroll Parent 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: scrollParent -//>>group: Core -//>>description: Get the closest ancestor element that is scrollable. -//>>docs: http://api.jqueryui.com/scrollParent/ - - - -var scrollParent = $.fn.scrollParent = function( includeHidden ) { - var position = this.css( "position" ), - excludeStaticParent = position === "absolute", - overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/, - scrollParent = this.parents().filter( function() { - var parent = $( this ); - if ( excludeStaticParent && parent.css( "position" ) === "static" ) { - return false; - } - return overflowRegex.test( parent.css( "overflow" ) + parent.css( "overflow-y" ) + - parent.css( "overflow-x" ) ); - } ).eq( 0 ); - - return position === "fixed" || !scrollParent.length ? - $( this[ 0 ].ownerDocument || document ) : - scrollParent; -}; - - /*! * jQuery UI Unique ID 1.12.1 * http://jqueryui.com @@ -1660,1716 +1625,6 @@ var safeBlur = $.ui.safeBlur = function( element ) { }; -/*! - * jQuery UI Draggable 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Draggable -//>>group: Interactions -//>>description: Enables dragging functionality for any element. -//>>docs: http://api.jqueryui.com/draggable/ -//>>demos: http://jqueryui.com/draggable/ -//>>css.structure: ../../themes/base/draggable.css - - - -$.widget( "ui.draggable", $.ui.mouse, { - version: "1.12.1", - widgetEventPrefix: "drag", - options: { - addClasses: true, - appendTo: "parent", - axis: false, - connectToSortable: false, - containment: false, - cursor: "auto", - cursorAt: false, - grid: false, - handle: false, - helper: "original", - iframeFix: false, - opacity: false, - refreshPositions: false, - revert: false, - revertDuration: 500, - scope: "default", - scroll: true, - scrollSensitivity: 20, - scrollSpeed: 20, - snap: false, - snapMode: "both", - snapTolerance: 20, - stack: false, - zIndex: false, - - // Callbacks - drag: null, - start: null, - stop: null - }, - _create: function() { - - if ( this.options.helper === "original" ) { - this._setPositionRelative(); - } - if ( this.options.addClasses ) { - this._addClass( "ui-draggable" ); - } - this._setHandleClassName(); - - this._mouseInit(); - }, - - _setOption: function( key, value ) { - this._super( key, value ); - if ( key === "handle" ) { - this._removeHandleClassName(); - this._setHandleClassName(); - } - }, - - _destroy: function() { - if ( ( this.helper || this.element ).is( ".ui-draggable-dragging" ) ) { - this.destroyOnClear = true; - return; - } - this._removeHandleClassName(); - this._mouseDestroy(); - }, - - _mouseCapture: function( event ) { - var o = this.options; - - // Among others, prevent a drag on a resizable-handle - if ( this.helper || o.disabled || - $( event.target ).closest( ".ui-resizable-handle" ).length > 0 ) { - return false; - } - - //Quit if we're not on a valid handle - this.handle = this._getHandle( event ); - if ( !this.handle ) { - return false; - } - - this._blurActiveElement( event ); - - this._blockFrames( o.iframeFix === true ? "iframe" : o.iframeFix ); - - return true; - - }, - - _blockFrames: function( selector ) { - this.iframeBlocks = this.document.find( selector ).map( function() { - var iframe = $( this ); - - return $( "
" ) - .css( "position", "absolute" ) - .appendTo( iframe.parent() ) - .outerWidth( iframe.outerWidth() ) - .outerHeight( iframe.outerHeight() ) - .offset( iframe.offset() )[ 0 ]; - } ); - }, - - _unblockFrames: function() { - if ( this.iframeBlocks ) { - this.iframeBlocks.remove(); - delete this.iframeBlocks; - } - }, - - _blurActiveElement: function( event ) { - var activeElement = $.ui.safeActiveElement( this.document[ 0 ] ), - target = $( event.target ); - - // Don't blur if the event occurred on an element that is within - // the currently focused element - // See #10527, #12472 - if ( target.closest( activeElement ).length ) { - return; - } - - // Blur any element that currently has focus, see #4261 - $.ui.safeBlur( activeElement ); - }, - - _mouseStart: function( event ) { - - var o = this.options; - - //Create and append the visible helper - this.helper = this._createHelper( event ); - - this._addClass( this.helper, "ui-draggable-dragging" ); - - //Cache the helper size - this._cacheHelperProportions(); - - //If ddmanager is used for droppables, set the global draggable - if ( $.ui.ddmanager ) { - $.ui.ddmanager.current = this; - } - - /* - * - Position generation - - * This block generates everything position related - it's the core of draggables. - */ - - //Cache the margins of the original element - this._cacheMargins(); - - //Store the helper's css position - this.cssPosition = this.helper.css( "position" ); - this.scrollParent = this.helper.scrollParent( true ); - this.offsetParent = this.helper.offsetParent(); - this.hasFixedAncestor = this.helper.parents().filter( function() { - return $( this ).css( "position" ) === "fixed"; - } ).length > 0; - - //The element's absolute position on the page minus margins - this.positionAbs = this.element.offset(); - this._refreshOffsets( event ); - - //Generate the original position - this.originalPosition = this.position = this._generatePosition( event, false ); - this.originalPageX = event.pageX; - this.originalPageY = event.pageY; - - //Adjust the mouse offset relative to the helper if "cursorAt" is supplied - ( o.cursorAt && this._adjustOffsetFromHelper( o.cursorAt ) ); - - //Set a containment if given in the options - this._setContainment(); - - //Trigger event + callbacks - if ( this._trigger( "start", event ) === false ) { - this._clear(); - return false; - } - - //Recache the helper size - this._cacheHelperProportions(); - - //Prepare the droppable offsets - if ( $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( this, event ); - } - - // Execute the drag once - this causes the helper not to be visible before getting its - // correct position - this._mouseDrag( event, true ); - - // If the ddmanager is used for droppables, inform the manager that dragging has started - // (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStart( this, event ); - } - - return true; - }, - - _refreshOffsets: function( event ) { - this.offset = { - top: this.positionAbs.top - this.margins.top, - left: this.positionAbs.left - this.margins.left, - scroll: false, - parent: this._getParentOffset(), - relative: this._getRelativeOffset() - }; - - this.offset.click = { - left: event.pageX - this.offset.left, - top: event.pageY - this.offset.top - }; - }, - - _mouseDrag: function( event, noPropagation ) { - - // reset any necessary cached properties (see #5009) - if ( this.hasFixedAncestor ) { - this.offset.parent = this._getParentOffset(); - } - - //Compute the helpers position - this.position = this._generatePosition( event, true ); - this.positionAbs = this._convertPositionTo( "absolute" ); - - //Call plugins and callbacks and use the resulting position if something is returned - if ( !noPropagation ) { - var ui = this._uiHash(); - if ( this._trigger( "drag", event, ui ) === false ) { - this._mouseUp( new $.Event( "mouseup", event ) ); - return false; - } - this.position = ui.position; - } - - this.helper[ 0 ].style.left = this.position.left + "px"; - this.helper[ 0 ].style.top = this.position.top + "px"; - - if ( $.ui.ddmanager ) { - $.ui.ddmanager.drag( this, event ); - } - - return false; - }, - - _mouseStop: function( event ) { - - //If we are using droppables, inform the manager about the drop - var that = this, - dropped = false; - if ( $.ui.ddmanager && !this.options.dropBehaviour ) { - dropped = $.ui.ddmanager.drop( this, event ); - } - - //if a drop comes from outside (a sortable) - if ( this.dropped ) { - dropped = this.dropped; - this.dropped = false; - } - - if ( ( this.options.revert === "invalid" && !dropped ) || - ( this.options.revert === "valid" && dropped ) || - this.options.revert === true || ( $.isFunction( this.options.revert ) && - this.options.revert.call( this.element, dropped ) ) - ) { - $( this.helper ).animate( - this.originalPosition, - parseInt( this.options.revertDuration, 10 ), - function() { - if ( that._trigger( "stop", event ) !== false ) { - that._clear(); - } - } - ); - } else { - if ( this._trigger( "stop", event ) !== false ) { - this._clear(); - } - } - - return false; - }, - - _mouseUp: function( event ) { - this._unblockFrames(); - - // If the ddmanager is used for droppables, inform the manager that dragging has stopped - // (see #5003) - if ( $.ui.ddmanager ) { - $.ui.ddmanager.dragStop( this, event ); - } - - // Only need to focus if the event occurred on the draggable itself, see #10527 - if ( this.handleElement.is( event.target ) ) { - - // The interaction is over; whether or not the click resulted in a drag, - // focus the element - this.element.trigger( "focus" ); - } - - return $.ui.mouse.prototype._mouseUp.call( this, event ); - }, - - cancel: function() { - - if ( this.helper.is( ".ui-draggable-dragging" ) ) { - this._mouseUp( new $.Event( "mouseup", { target: this.element[ 0 ] } ) ); - } else { - this._clear(); - } - - return this; - - }, - - _getHandle: function( event ) { - return this.options.handle ? - !!$( event.target ).closest( this.element.find( this.options.handle ) ).length : - true; - }, - - _setHandleClassName: function() { - this.handleElement = this.options.handle ? - this.element.find( this.options.handle ) : this.element; - this._addClass( this.handleElement, "ui-draggable-handle" ); - }, - - _removeHandleClassName: function() { - this._removeClass( this.handleElement, "ui-draggable-handle" ); - }, - - _createHelper: function( event ) { - - var o = this.options, - helperIsFunction = $.isFunction( o.helper ), - helper = helperIsFunction ? - $( o.helper.apply( this.element[ 0 ], [ event ] ) ) : - ( o.helper === "clone" ? - this.element.clone().removeAttr( "id" ) : - this.element ); - - if ( !helper.parents( "body" ).length ) { - helper.appendTo( ( o.appendTo === "parent" ? - this.element[ 0 ].parentNode : - o.appendTo ) ); - } - - // Http://bugs.jqueryui.com/ticket/9446 - // a helper function can return the original element - // which wouldn't have been set to relative in _create - if ( helperIsFunction && helper[ 0 ] === this.element[ 0 ] ) { - this._setPositionRelative(); - } - - if ( helper[ 0 ] !== this.element[ 0 ] && - !( /(fixed|absolute)/ ).test( helper.css( "position" ) ) ) { - helper.css( "position", "absolute" ); - } - - return helper; - - }, - - _setPositionRelative: function() { - if ( !( /^(?:r|a|f)/ ).test( this.element.css( "position" ) ) ) { - this.element[ 0 ].style.position = "relative"; - } - }, - - _adjustOffsetFromHelper: function( obj ) { - if ( typeof obj === "string" ) { - obj = obj.split( " " ); - } - if ( $.isArray( obj ) ) { - obj = { left: +obj[ 0 ], top: +obj[ 1 ] || 0 }; - } - if ( "left" in obj ) { - this.offset.click.left = obj.left + this.margins.left; - } - if ( "right" in obj ) { - this.offset.click.left = this.helperProportions.width - obj.right + this.margins.left; - } - if ( "top" in obj ) { - this.offset.click.top = obj.top + this.margins.top; - } - if ( "bottom" in obj ) { - this.offset.click.top = this.helperProportions.height - obj.bottom + this.margins.top; - } - }, - - _isRootNode: function( element ) { - return ( /(html|body)/i ).test( element.tagName ) || element === this.document[ 0 ]; - }, - - _getParentOffset: function() { - - //Get the offsetParent and cache its position - var po = this.offsetParent.offset(), - document = this.document[ 0 ]; - - // This is a special case where we need to modify a offset calculated on start, since the - // following happened: - // 1. The position of the helper is absolute, so it's position is calculated based on the - // next positioned parent - // 2. The actual offset parent is a child of the scroll parent, and the scroll parent isn't - // the document, which means that the scroll is included in the initial calculation of the - // offset of the parent, and never recalculated upon drag - if ( this.cssPosition === "absolute" && this.scrollParent[ 0 ] !== document && - $.contains( this.scrollParent[ 0 ], this.offsetParent[ 0 ] ) ) { - po.left += this.scrollParent.scrollLeft(); - po.top += this.scrollParent.scrollTop(); - } - - if ( this._isRootNode( this.offsetParent[ 0 ] ) ) { - po = { top: 0, left: 0 }; - } - - return { - top: po.top + ( parseInt( this.offsetParent.css( "borderTopWidth" ), 10 ) || 0 ), - left: po.left + ( parseInt( this.offsetParent.css( "borderLeftWidth" ), 10 ) || 0 ) - }; - - }, - - _getRelativeOffset: function() { - if ( this.cssPosition !== "relative" ) { - return { top: 0, left: 0 }; - } - - var p = this.element.position(), - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); - - return { - top: p.top - ( parseInt( this.helper.css( "top" ), 10 ) || 0 ) + - ( !scrollIsRootNode ? this.scrollParent.scrollTop() : 0 ), - left: p.left - ( parseInt( this.helper.css( "left" ), 10 ) || 0 ) + - ( !scrollIsRootNode ? this.scrollParent.scrollLeft() : 0 ) - }; - - }, - - _cacheMargins: function() { - this.margins = { - left: ( parseInt( this.element.css( "marginLeft" ), 10 ) || 0 ), - top: ( parseInt( this.element.css( "marginTop" ), 10 ) || 0 ), - right: ( parseInt( this.element.css( "marginRight" ), 10 ) || 0 ), - bottom: ( parseInt( this.element.css( "marginBottom" ), 10 ) || 0 ) - }; - }, - - _cacheHelperProportions: function() { - this.helperProportions = { - width: this.helper.outerWidth(), - height: this.helper.outerHeight() - }; - }, - - _setContainment: function() { - - var isUserScrollable, c, ce, - o = this.options, - document = this.document[ 0 ]; - - this.relativeContainer = null; - - if ( !o.containment ) { - this.containment = null; - return; - } - - if ( o.containment === "window" ) { - this.containment = [ - $( window ).scrollLeft() - this.offset.relative.left - this.offset.parent.left, - $( window ).scrollTop() - this.offset.relative.top - this.offset.parent.top, - $( window ).scrollLeft() + $( window ).width() - - this.helperProportions.width - this.margins.left, - $( window ).scrollTop() + - ( $( window ).height() || document.body.parentNode.scrollHeight ) - - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment === "document" ) { - this.containment = [ - 0, - 0, - $( document ).width() - this.helperProportions.width - this.margins.left, - ( $( document ).height() || document.body.parentNode.scrollHeight ) - - this.helperProportions.height - this.margins.top - ]; - return; - } - - if ( o.containment.constructor === Array ) { - this.containment = o.containment; - return; - } - - if ( o.containment === "parent" ) { - o.containment = this.helper[ 0 ].parentNode; - } - - c = $( o.containment ); - ce = c[ 0 ]; - - if ( !ce ) { - return; - } - - isUserScrollable = /(scroll|auto)/.test( c.css( "overflow" ) ); - - this.containment = [ - ( parseInt( c.css( "borderLeftWidth" ), 10 ) || 0 ) + - ( parseInt( c.css( "paddingLeft" ), 10 ) || 0 ), - ( parseInt( c.css( "borderTopWidth" ), 10 ) || 0 ) + - ( parseInt( c.css( "paddingTop" ), 10 ) || 0 ), - ( isUserScrollable ? Math.max( ce.scrollWidth, ce.offsetWidth ) : ce.offsetWidth ) - - ( parseInt( c.css( "borderRightWidth" ), 10 ) || 0 ) - - ( parseInt( c.css( "paddingRight" ), 10 ) || 0 ) - - this.helperProportions.width - - this.margins.left - - this.margins.right, - ( isUserScrollable ? Math.max( ce.scrollHeight, ce.offsetHeight ) : ce.offsetHeight ) - - ( parseInt( c.css( "borderBottomWidth" ), 10 ) || 0 ) - - ( parseInt( c.css( "paddingBottom" ), 10 ) || 0 ) - - this.helperProportions.height - - this.margins.top - - this.margins.bottom - ]; - this.relativeContainer = c; - }, - - _convertPositionTo: function( d, pos ) { - - if ( !pos ) { - pos = this.position; - } - - var mod = d === "absolute" ? 1 : -1, - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ); - - return { - top: ( - - // The absolute mouse position - pos.top + - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top * mod + - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top * mod - - ( ( this.cssPosition === "fixed" ? - -this.offset.scroll.top : - ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) * mod ) - ), - left: ( - - // The absolute mouse position - pos.left + - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left * mod + - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left * mod - - ( ( this.cssPosition === "fixed" ? - -this.offset.scroll.left : - ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) * mod ) - ) - }; - - }, - - _generatePosition: function( event, constrainPosition ) { - - var containment, co, top, left, - o = this.options, - scrollIsRootNode = this._isRootNode( this.scrollParent[ 0 ] ), - pageX = event.pageX, - pageY = event.pageY; - - // Cache the scroll - if ( !scrollIsRootNode || !this.offset.scroll ) { - this.offset.scroll = { - top: this.scrollParent.scrollTop(), - left: this.scrollParent.scrollLeft() - }; - } - - /* - * - Position constraining - - * Constrain the position to a mix of grid, containment. - */ - - // If we are not dragging yet, we won't check for options - if ( constrainPosition ) { - if ( this.containment ) { - if ( this.relativeContainer ) { - co = this.relativeContainer.offset(); - containment = [ - this.containment[ 0 ] + co.left, - this.containment[ 1 ] + co.top, - this.containment[ 2 ] + co.left, - this.containment[ 3 ] + co.top - ]; - } else { - containment = this.containment; - } - - if ( event.pageX - this.offset.click.left < containment[ 0 ] ) { - pageX = containment[ 0 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top < containment[ 1 ] ) { - pageY = containment[ 1 ] + this.offset.click.top; - } - if ( event.pageX - this.offset.click.left > containment[ 2 ] ) { - pageX = containment[ 2 ] + this.offset.click.left; - } - if ( event.pageY - this.offset.click.top > containment[ 3 ] ) { - pageY = containment[ 3 ] + this.offset.click.top; - } - } - - if ( o.grid ) { - - //Check for grid elements set to 0 to prevent divide by 0 error causing invalid - // argument errors in IE (see ticket #6950) - top = o.grid[ 1 ] ? this.originalPageY + Math.round( ( pageY - - this.originalPageY ) / o.grid[ 1 ] ) * o.grid[ 1 ] : this.originalPageY; - pageY = containment ? ( ( top - this.offset.click.top >= containment[ 1 ] || - top - this.offset.click.top > containment[ 3 ] ) ? - top : - ( ( top - this.offset.click.top >= containment[ 1 ] ) ? - top - o.grid[ 1 ] : top + o.grid[ 1 ] ) ) : top; - - left = o.grid[ 0 ] ? this.originalPageX + - Math.round( ( pageX - this.originalPageX ) / o.grid[ 0 ] ) * o.grid[ 0 ] : - this.originalPageX; - pageX = containment ? ( ( left - this.offset.click.left >= containment[ 0 ] || - left - this.offset.click.left > containment[ 2 ] ) ? - left : - ( ( left - this.offset.click.left >= containment[ 0 ] ) ? - left - o.grid[ 0 ] : left + o.grid[ 0 ] ) ) : left; - } - - if ( o.axis === "y" ) { - pageX = this.originalPageX; - } - - if ( o.axis === "x" ) { - pageY = this.originalPageY; - } - } - - return { - top: ( - - // The absolute mouse position - pageY - - - // Click offset (relative to the element) - this.offset.click.top - - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.top - - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.top + - ( this.cssPosition === "fixed" ? - -this.offset.scroll.top : - ( scrollIsRootNode ? 0 : this.offset.scroll.top ) ) - ), - left: ( - - // The absolute mouse position - pageX - - - // Click offset (relative to the element) - this.offset.click.left - - - // Only for relative positioned nodes: Relative offset from element to offset parent - this.offset.relative.left - - - // The offsetParent's offset without borders (offset + border) - this.offset.parent.left + - ( this.cssPosition === "fixed" ? - -this.offset.scroll.left : - ( scrollIsRootNode ? 0 : this.offset.scroll.left ) ) - ) - }; - - }, - - _clear: function() { - this._removeClass( this.helper, "ui-draggable-dragging" ); - if ( this.helper[ 0 ] !== this.element[ 0 ] && !this.cancelHelperRemoval ) { - this.helper.remove(); - } - this.helper = null; - this.cancelHelperRemoval = false; - if ( this.destroyOnClear ) { - this.destroy(); - } - }, - - // From now on bulk stuff - mainly helpers - - _trigger: function( type, event, ui ) { - ui = ui || this._uiHash(); - $.ui.plugin.call( this, type, [ event, ui, this ], true ); - - // Absolute position and offset (see #6884 ) have to be recalculated after plugins - if ( /^(drag|start|stop)/.test( type ) ) { - this.positionAbs = this._convertPositionTo( "absolute" ); - ui.offset = this.positionAbs; - } - return $.Widget.prototype._trigger.call( this, type, event, ui ); - }, - - plugins: {}, - - _uiHash: function() { - return { - helper: this.helper, - position: this.position, - originalPosition: this.originalPosition, - offset: this.positionAbs - }; - } - -} ); - -$.ui.plugin.add( "draggable", "connectToSortable", { - start: function( event, ui, draggable ) { - var uiSortable = $.extend( {}, ui, { - item: draggable.element - } ); - - draggable.sortables = []; - $( draggable.options.connectToSortable ).each( function() { - var sortable = $( this ).sortable( "instance" ); - - if ( sortable && !sortable.options.disabled ) { - draggable.sortables.push( sortable ); - - // RefreshPositions is called at drag start to refresh the containerCache - // which is used in drag. This ensures it's initialized and synchronized - // with any changes that might have happened on the page since initialization. - sortable.refreshPositions(); - sortable._trigger( "activate", event, uiSortable ); - } - } ); - }, - stop: function( event, ui, draggable ) { - var uiSortable = $.extend( {}, ui, { - item: draggable.element - } ); - - draggable.cancelHelperRemoval = false; - - $.each( draggable.sortables, function() { - var sortable = this; - - if ( sortable.isOver ) { - sortable.isOver = 0; - - // Allow this sortable to handle removing the helper - draggable.cancelHelperRemoval = true; - sortable.cancelHelperRemoval = false; - - // Use _storedCSS To restore properties in the sortable, - // as this also handles revert (#9675) since the draggable - // may have modified them in unexpected ways (#8809) - sortable._storedCSS = { - position: sortable.placeholder.css( "position" ), - top: sortable.placeholder.css( "top" ), - left: sortable.placeholder.css( "left" ) - }; - - sortable._mouseStop( event ); - - // Once drag has ended, the sortable should return to using - // its original helper, not the shared helper from draggable - sortable.options.helper = sortable.options._helper; - } else { - - // Prevent this Sortable from removing the helper. - // However, don't set the draggable to remove the helper - // either as another connected Sortable may yet handle the removal. - sortable.cancelHelperRemoval = true; - - sortable._trigger( "deactivate", event, uiSortable ); - } - } ); - }, - drag: function( event, ui, draggable ) { - $.each( draggable.sortables, function() { - var innermostIntersecting = false, - sortable = this; - - // Copy over variables that sortable's _intersectsWith uses - sortable.positionAbs = draggable.positionAbs; - sortable.helperProportions = draggable.helperProportions; - sortable.offset.click = draggable.offset.click; - - if ( sortable._intersectsWith( sortable.containerCache ) ) { - innermostIntersecting = true; - - $.each( draggable.sortables, function() { - - // Copy over variables that sortable's _intersectsWith uses - this.positionAbs = draggable.positionAbs; - this.helperProportions = draggable.helperProportions; - this.offset.click = draggable.offset.click; - - if ( this !== sortable && - this._intersectsWith( this.containerCache ) && - $.contains( sortable.element[ 0 ], this.element[ 0 ] ) ) { - innermostIntersecting = false; - } - - return innermostIntersecting; - } ); - } - - if ( innermostIntersecting ) { - - // If it intersects, we use a little isOver variable and set it once, - // so that the move-in stuff gets fired only once. - if ( !sortable.isOver ) { - sortable.isOver = 1; - - // Store draggable's parent in case we need to reappend to it later. - draggable._parent = ui.helper.parent(); - - sortable.currentItem = ui.helper - .appendTo( sortable.element ) - .data( "ui-sortable-item", true ); - - // Store helper option to later restore it - sortable.options._helper = sortable.options.helper; - - sortable.options.helper = function() { - return ui.helper[ 0 ]; - }; - - // Fire the start events of the sortable with our passed browser event, - // and our own helper (so it doesn't create a new one) - event.target = sortable.currentItem[ 0 ]; - sortable._mouseCapture( event, true ); - sortable._mouseStart( event, true, true ); - - // Because the browser event is way off the new appended portlet, - // modify necessary variables to reflect the changes - sortable.offset.click.top = draggable.offset.click.top; - sortable.offset.click.left = draggable.offset.click.left; - sortable.offset.parent.left -= draggable.offset.parent.left - - sortable.offset.parent.left; - sortable.offset.parent.top -= draggable.offset.parent.top - - sortable.offset.parent.top; - - draggable._trigger( "toSortable", event ); - - // Inform draggable that the helper is in a valid drop zone, - // used solely in the revert option to handle "valid/invalid". - draggable.dropped = sortable.element; - - // Need to refreshPositions of all sortables in the case that - // adding to one sortable changes the location of the other sortables (#9675) - $.each( draggable.sortables, function() { - this.refreshPositions(); - } ); - - // Hack so receive/update callbacks work (mostly) - draggable.currentItem = draggable.element; - sortable.fromOutside = draggable; - } - - if ( sortable.currentItem ) { - sortable._mouseDrag( event ); - - // Copy the sortable's position because the draggable's can potentially reflect - // a relative position, while sortable is always absolute, which the dragged - // element has now become. (#8809) - ui.position = sortable.position; - } - } else { - - // If it doesn't intersect with the sortable, and it intersected before, - // we fake the drag stop of the sortable, but make sure it doesn't remove - // the helper by using cancelHelperRemoval. - if ( sortable.isOver ) { - - sortable.isOver = 0; - sortable.cancelHelperRemoval = true; - - // Calling sortable's mouseStop would trigger a revert, - // so revert must be temporarily false until after mouseStop is called. - sortable.options._revert = sortable.options.revert; - sortable.options.revert = false; - - sortable._trigger( "out", event, sortable._uiHash( sortable ) ); - sortable._mouseStop( event, true ); - - // Restore sortable behaviors that were modfied - // when the draggable entered the sortable area (#9481) - sortable.options.revert = sortable.options._revert; - sortable.options.helper = sortable.options._helper; - - if ( sortable.placeholder ) { - sortable.placeholder.remove(); - } - - // Restore and recalculate the draggable's offset considering the sortable - // may have modified them in unexpected ways. (#8809, #10669) - ui.helper.appendTo( draggable._parent ); - draggable._refreshOffsets( event ); - ui.position = draggable._generatePosition( event, true ); - - draggable._trigger( "fromSortable", event ); - - // Inform draggable that the helper is no longer in a valid drop zone - draggable.dropped = false; - - // Need to refreshPositions of all sortables just in case removing - // from one sortable changes the location of other sortables (#9675) - $.each( draggable.sortables, function() { - this.refreshPositions(); - } ); - } - } - } ); - } -} ); - -$.ui.plugin.add( "draggable", "cursor", { - start: function( event, ui, instance ) { - var t = $( "body" ), - o = instance.options; - - if ( t.css( "cursor" ) ) { - o._cursor = t.css( "cursor" ); - } - t.css( "cursor", o.cursor ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; - if ( o._cursor ) { - $( "body" ).css( "cursor", o._cursor ); - } - } -} ); - -$.ui.plugin.add( "draggable", "opacity", { - start: function( event, ui, instance ) { - var t = $( ui.helper ), - o = instance.options; - if ( t.css( "opacity" ) ) { - o._opacity = t.css( "opacity" ); - } - t.css( "opacity", o.opacity ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; - if ( o._opacity ) { - $( ui.helper ).css( "opacity", o._opacity ); - } - } -} ); - -$.ui.plugin.add( "draggable", "scroll", { - start: function( event, ui, i ) { - if ( !i.scrollParentNotHidden ) { - i.scrollParentNotHidden = i.helper.scrollParent( false ); - } - - if ( i.scrollParentNotHidden[ 0 ] !== i.document[ 0 ] && - i.scrollParentNotHidden[ 0 ].tagName !== "HTML" ) { - i.overflowOffset = i.scrollParentNotHidden.offset(); - } - }, - drag: function( event, ui, i ) { - - var o = i.options, - scrolled = false, - scrollParent = i.scrollParentNotHidden[ 0 ], - document = i.document[ 0 ]; - - if ( scrollParent !== document && scrollParent.tagName !== "HTML" ) { - if ( !o.axis || o.axis !== "x" ) { - if ( ( i.overflowOffset.top + scrollParent.offsetHeight ) - event.pageY < - o.scrollSensitivity ) { - scrollParent.scrollTop = scrolled = scrollParent.scrollTop + o.scrollSpeed; - } else if ( event.pageY - i.overflowOffset.top < o.scrollSensitivity ) { - scrollParent.scrollTop = scrolled = scrollParent.scrollTop - o.scrollSpeed; - } - } - - if ( !o.axis || o.axis !== "y" ) { - if ( ( i.overflowOffset.left + scrollParent.offsetWidth ) - event.pageX < - o.scrollSensitivity ) { - scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft + o.scrollSpeed; - } else if ( event.pageX - i.overflowOffset.left < o.scrollSensitivity ) { - scrollParent.scrollLeft = scrolled = scrollParent.scrollLeft - o.scrollSpeed; - } - } - - } else { - - if ( !o.axis || o.axis !== "x" ) { - if ( event.pageY - $( document ).scrollTop() < o.scrollSensitivity ) { - scrolled = $( document ).scrollTop( $( document ).scrollTop() - o.scrollSpeed ); - } else if ( $( window ).height() - ( event.pageY - $( document ).scrollTop() ) < - o.scrollSensitivity ) { - scrolled = $( document ).scrollTop( $( document ).scrollTop() + o.scrollSpeed ); - } - } - - if ( !o.axis || o.axis !== "y" ) { - if ( event.pageX - $( document ).scrollLeft() < o.scrollSensitivity ) { - scrolled = $( document ).scrollLeft( - $( document ).scrollLeft() - o.scrollSpeed - ); - } else if ( $( window ).width() - ( event.pageX - $( document ).scrollLeft() ) < - o.scrollSensitivity ) { - scrolled = $( document ).scrollLeft( - $( document ).scrollLeft() + o.scrollSpeed - ); - } - } - - } - - if ( scrolled !== false && $.ui.ddmanager && !o.dropBehaviour ) { - $.ui.ddmanager.prepareOffsets( i, event ); - } - - } -} ); - -$.ui.plugin.add( "draggable", "snap", { - start: function( event, ui, i ) { - - var o = i.options; - - i.snapElements = []; - - $( o.snap.constructor !== String ? ( o.snap.items || ":data(ui-draggable)" ) : o.snap ) - .each( function() { - var $t = $( this ), - $o = $t.offset(); - if ( this !== i.element[ 0 ] ) { - i.snapElements.push( { - item: this, - width: $t.outerWidth(), height: $t.outerHeight(), - top: $o.top, left: $o.left - } ); - } - } ); - - }, - drag: function( event, ui, inst ) { - - var ts, bs, ls, rs, l, r, t, b, i, first, - o = inst.options, - d = o.snapTolerance, - x1 = ui.offset.left, x2 = x1 + inst.helperProportions.width, - y1 = ui.offset.top, y2 = y1 + inst.helperProportions.height; - - for ( i = inst.snapElements.length - 1; i >= 0; i-- ) { - - l = inst.snapElements[ i ].left - inst.margins.left; - r = l + inst.snapElements[ i ].width; - t = inst.snapElements[ i ].top - inst.margins.top; - b = t + inst.snapElements[ i ].height; - - if ( x2 < l - d || x1 > r + d || y2 < t - d || y1 > b + d || - !$.contains( inst.snapElements[ i ].item.ownerDocument, - inst.snapElements[ i ].item ) ) { - if ( inst.snapElements[ i ].snapping ) { - ( inst.options.snap.release && - inst.options.snap.release.call( - inst.element, - event, - $.extend( inst._uiHash(), { snapItem: inst.snapElements[ i ].item } ) - ) ); - } - inst.snapElements[ i ].snapping = false; - continue; - } - - if ( o.snapMode !== "inner" ) { - ts = Math.abs( t - y2 ) <= d; - bs = Math.abs( b - y1 ) <= d; - ls = Math.abs( l - x2 ) <= d; - rs = Math.abs( r - x1 ) <= d; - if ( ts ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: t - inst.helperProportions.height, - left: 0 - } ).top; - } - if ( bs ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: b, - left: 0 - } ).top; - } - if ( ls ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: l - inst.helperProportions.width - } ).left; - } - if ( rs ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: r - } ).left; - } - } - - first = ( ts || bs || ls || rs ); - - if ( o.snapMode !== "outer" ) { - ts = Math.abs( t - y1 ) <= d; - bs = Math.abs( b - y2 ) <= d; - ls = Math.abs( l - x1 ) <= d; - rs = Math.abs( r - x2 ) <= d; - if ( ts ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: t, - left: 0 - } ).top; - } - if ( bs ) { - ui.position.top = inst._convertPositionTo( "relative", { - top: b - inst.helperProportions.height, - left: 0 - } ).top; - } - if ( ls ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: l - } ).left; - } - if ( rs ) { - ui.position.left = inst._convertPositionTo( "relative", { - top: 0, - left: r - inst.helperProportions.width - } ).left; - } - } - - if ( !inst.snapElements[ i ].snapping && ( ts || bs || ls || rs || first ) ) { - ( inst.options.snap.snap && - inst.options.snap.snap.call( - inst.element, - event, - $.extend( inst._uiHash(), { - snapItem: inst.snapElements[ i ].item - } ) ) ); - } - inst.snapElements[ i ].snapping = ( ts || bs || ls || rs || first ); - - } - - } -} ); - -$.ui.plugin.add( "draggable", "stack", { - start: function( event, ui, instance ) { - var min, - o = instance.options, - group = $.makeArray( $( o.stack ) ).sort( function( a, b ) { - return ( parseInt( $( a ).css( "zIndex" ), 10 ) || 0 ) - - ( parseInt( $( b ).css( "zIndex" ), 10 ) || 0 ); - } ); - - if ( !group.length ) { return; } - - min = parseInt( $( group[ 0 ] ).css( "zIndex" ), 10 ) || 0; - $( group ).each( function( i ) { - $( this ).css( "zIndex", min + i ); - } ); - this.css( "zIndex", ( min + group.length ) ); - } -} ); - -$.ui.plugin.add( "draggable", "zIndex", { - start: function( event, ui, instance ) { - var t = $( ui.helper ), - o = instance.options; - - if ( t.css( "zIndex" ) ) { - o._zIndex = t.css( "zIndex" ); - } - t.css( "zIndex", o.zIndex ); - }, - stop: function( event, ui, instance ) { - var o = instance.options; - - if ( o._zIndex ) { - $( ui.helper ).css( "zIndex", o._zIndex ); - } - } -} ); - -var widgetsDraggable = $.ui.draggable; - - -/*! - * jQuery UI Droppable 1.12.1 - * http://jqueryui.com - * - * Copyright jQuery Foundation and other contributors - * Released under the MIT license. - * http://jquery.org/license - */ - -//>>label: Droppable -//>>group: Interactions -//>>description: Enables drop targets for draggable elements. -//>>docs: http://api.jqueryui.com/droppable/ -//>>demos: http://jqueryui.com/droppable/ - - - -$.widget( "ui.droppable", { - version: "1.12.1", - widgetEventPrefix: "drop", - options: { - accept: "*", - addClasses: true, - greedy: false, - scope: "default", - tolerance: "intersect", - - // Callbacks - activate: null, - deactivate: null, - drop: null, - out: null, - over: null - }, - _create: function() { - - var proportions, - o = this.options, - accept = o.accept; - - this.isover = false; - this.isout = true; - - this.accept = $.isFunction( accept ) ? accept : function( d ) { - return d.is( accept ); - }; - - this.proportions = function( /* valueToWrite */ ) { - if ( arguments.length ) { - - // Store the droppable's proportions - proportions = arguments[ 0 ]; - } else { - - // Retrieve or derive the droppable's proportions - return proportions ? - proportions : - proportions = { - width: this.element[ 0 ].offsetWidth, - height: this.element[ 0 ].offsetHeight - }; - } - }; - - this._addToManager( o.scope ); - - o.addClasses && this._addClass( "ui-droppable" ); - - }, - - _addToManager: function( scope ) { - - // Add the reference and positions to the manager - $.ui.ddmanager.droppables[ scope ] = $.ui.ddmanager.droppables[ scope ] || []; - $.ui.ddmanager.droppables[ scope ].push( this ); - }, - - _splice: function( drop ) { - var i = 0; - for ( ; i < drop.length; i++ ) { - if ( drop[ i ] === this ) { - drop.splice( i, 1 ); - } - } - }, - - _destroy: function() { - var drop = $.ui.ddmanager.droppables[ this.options.scope ]; - - this._splice( drop ); - }, - - _setOption: function( key, value ) { - - if ( key === "accept" ) { - this.accept = $.isFunction( value ) ? value : function( d ) { - return d.is( value ); - }; - } else if ( key === "scope" ) { - var drop = $.ui.ddmanager.droppables[ this.options.scope ]; - - this._splice( drop ); - this._addToManager( value ); - } - - this._super( key, value ); - }, - - _activate: function( event ) { - var draggable = $.ui.ddmanager.current; - - this._addActiveClass(); - if ( draggable ) { - this._trigger( "activate", event, this.ui( draggable ) ); - } - }, - - _deactivate: function( event ) { - var draggable = $.ui.ddmanager.current; - - this._removeActiveClass(); - if ( draggable ) { - this._trigger( "deactivate", event, this.ui( draggable ) ); - } - }, - - _over: function( event ) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return; - } - - if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || - draggable.element ) ) ) { - this._addHoverClass(); - this._trigger( "over", event, this.ui( draggable ) ); - } - - }, - - _out: function( event ) { - - var draggable = $.ui.ddmanager.current; - - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return; - } - - if ( this.accept.call( this.element[ 0 ], ( draggable.currentItem || - draggable.element ) ) ) { - this._removeHoverClass(); - this._trigger( "out", event, this.ui( draggable ) ); - } - - }, - - _drop: function( event, custom ) { - - var draggable = custom || $.ui.ddmanager.current, - childrenIntersection = false; - - // Bail if draggable and droppable are same element - if ( !draggable || ( draggable.currentItem || - draggable.element )[ 0 ] === this.element[ 0 ] ) { - return false; - } - - this.element - .find( ":data(ui-droppable)" ) - .not( ".ui-draggable-dragging" ) - .each( function() { - var inst = $( this ).droppable( "instance" ); - if ( - inst.options.greedy && - !inst.options.disabled && - inst.options.scope === draggable.options.scope && - inst.accept.call( - inst.element[ 0 ], ( draggable.currentItem || draggable.element ) - ) && - intersect( - draggable, - $.extend( inst, { offset: inst.element.offset() } ), - inst.options.tolerance, event - ) - ) { - childrenIntersection = true; - return false; } - } ); - if ( childrenIntersection ) { - return false; - } - - if ( this.accept.call( this.element[ 0 ], - ( draggable.currentItem || draggable.element ) ) ) { - this._removeActiveClass(); - this._removeHoverClass(); - - this._trigger( "drop", event, this.ui( draggable ) ); - return this.element; - } - - return false; - - }, - - ui: function( c ) { - return { - draggable: ( c.currentItem || c.element ), - helper: c.helper, - position: c.position, - offset: c.positionAbs - }; - }, - - // Extension points just to make backcompat sane and avoid duplicating logic - // TODO: Remove in 1.13 along with call to it below - _addHoverClass: function() { - this._addClass( "ui-droppable-hover" ); - }, - - _removeHoverClass: function() { - this._removeClass( "ui-droppable-hover" ); - }, - - _addActiveClass: function() { - this._addClass( "ui-droppable-active" ); - }, - - _removeActiveClass: function() { - this._removeClass( "ui-droppable-active" ); - } -} ); - -var intersect = $.ui.intersect = ( function() { - function isOverAxis( x, reference, size ) { - return ( x >= reference ) && ( x < ( reference + size ) ); - } - - return function( draggable, droppable, toleranceMode, event ) { - - if ( !droppable.offset ) { - return false; - } - - var x1 = ( draggable.positionAbs || - draggable.position.absolute ).left + draggable.margins.left, - y1 = ( draggable.positionAbs || - draggable.position.absolute ).top + draggable.margins.top, - x2 = x1 + draggable.helperProportions.width, - y2 = y1 + draggable.helperProportions.height, - l = droppable.offset.left, - t = droppable.offset.top, - r = l + droppable.proportions().width, - b = t + droppable.proportions().height; - - switch ( toleranceMode ) { - case "fit": - return ( l <= x1 && x2 <= r && t <= y1 && y2 <= b ); - case "intersect": - return ( l < x1 + ( draggable.helperProportions.width / 2 ) && // Right Half - x2 - ( draggable.helperProportions.width / 2 ) < r && // Left Half - t < y1 + ( draggable.helperProportions.height / 2 ) && // Bottom Half - y2 - ( draggable.helperProportions.height / 2 ) < b ); // Top Half - case "pointer": - return isOverAxis( event.pageY, t, droppable.proportions().height ) && - isOverAxis( event.pageX, l, droppable.proportions().width ); - case "touch": - return ( - ( y1 >= t && y1 <= b ) || // Top edge touching - ( y2 >= t && y2 <= b ) || // Bottom edge touching - ( y1 < t && y2 > b ) // Surrounded vertically - ) && ( - ( x1 >= l && x1 <= r ) || // Left edge touching - ( x2 >= l && x2 <= r ) || // Right edge touching - ( x1 < l && x2 > r ) // Surrounded horizontally - ); - default: - return false; - } - }; -} )(); - -/* - This manager tracks offsets of draggables and droppables -*/ -$.ui.ddmanager = { - current: null, - droppables: { "default": [] }, - prepareOffsets: function( t, event ) { - - var i, j, - m = $.ui.ddmanager.droppables[ t.options.scope ] || [], - type = event ? event.type : null, // workaround for #2317 - list = ( t.currentItem || t.element ).find( ":data(ui-droppable)" ).addBack(); - - droppablesLoop: for ( i = 0; i < m.length; i++ ) { - - // No disabled and non-accepted - if ( m[ i ].options.disabled || ( t && !m[ i ].accept.call( m[ i ].element[ 0 ], - ( t.currentItem || t.element ) ) ) ) { - continue; - } - - // Filter out elements in the current dragged item - for ( j = 0; j < list.length; j++ ) { - if ( list[ j ] === m[ i ].element[ 0 ] ) { - m[ i ].proportions().height = 0; - continue droppablesLoop; - } - } - - m[ i ].visible = m[ i ].element.css( "display" ) !== "none"; - if ( !m[ i ].visible ) { - continue; - } - - // Activate the droppable if used directly from draggables - if ( type === "mousedown" ) { - m[ i ]._activate.call( m[ i ], event ); - } - - m[ i ].offset = m[ i ].element.offset(); - m[ i ].proportions( { - width: m[ i ].element[ 0 ].offsetWidth, - height: m[ i ].element[ 0 ].offsetHeight - } ); - - } - - }, - drop: function( draggable, event ) { - - var dropped = false; - - // Create a copy of the droppables in case the list changes during the drop (#9116) - $.each( ( $.ui.ddmanager.droppables[ draggable.options.scope ] || [] ).slice(), function() { - - if ( !this.options ) { - return; - } - if ( !this.options.disabled && this.visible && - intersect( draggable, this, this.options.tolerance, event ) ) { - dropped = this._drop.call( this, event ) || dropped; - } - - if ( !this.options.disabled && this.visible && this.accept.call( this.element[ 0 ], - ( draggable.currentItem || draggable.element ) ) ) { - this.isout = true; - this.isover = false; - this._deactivate.call( this, event ); - } - - } ); - return dropped; - - }, - dragStart: function( draggable, event ) { - - // Listen for scrolling so that if the dragging causes scrolling the position of the - // droppables can be recalculated (see #5003) - draggable.element.parentsUntil( "body" ).on( "scroll.droppable", function() { - if ( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } ); - }, - drag: function( draggable, event ) { - - // If you have a highly dynamic page, you might try this option. It renders positions - // every time you move the mouse. - if ( draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - - // Run through all droppables and check their positions based on specific tolerance options - $.each( $.ui.ddmanager.droppables[ draggable.options.scope ] || [], function() { - - if ( this.options.disabled || this.greedyChild || !this.visible ) { - return; - } - - var parentInstance, scope, parent, - intersects = intersect( draggable, this, this.options.tolerance, event ), - c = !intersects && this.isover ? - "isout" : - ( intersects && !this.isover ? "isover" : null ); - if ( !c ) { - return; - } - - if ( this.options.greedy ) { - - // find droppable parents with same scope - scope = this.options.scope; - parent = this.element.parents( ":data(ui-droppable)" ).filter( function() { - return $( this ).droppable( "instance" ).options.scope === scope; - } ); - - if ( parent.length ) { - parentInstance = $( parent[ 0 ] ).droppable( "instance" ); - parentInstance.greedyChild = ( c === "isover" ); - } - } - - // We just moved into a greedy child - if ( parentInstance && c === "isover" ) { - parentInstance.isover = false; - parentInstance.isout = true; - parentInstance._out.call( parentInstance, event ); - } - - this[ c ] = true; - this[ c === "isout" ? "isover" : "isout" ] = false; - this[ c === "isover" ? "_over" : "_out" ].call( this, event ); - - // We just moved out of a greedy child - if ( parentInstance && c === "isout" ) { - parentInstance.isout = false; - parentInstance.isover = true; - parentInstance._over.call( parentInstance, event ); - } - } ); - - }, - dragStop: function( draggable, event ) { - draggable.element.parentsUntil( "body" ).off( "scroll.droppable" ); - - // Call prepareOffsets one final time since IE does not fire return scroll events when - // overflow was caused by drag (see #5003) - if ( !draggable.options.refreshPositions ) { - $.ui.ddmanager.prepareOffsets( draggable, event ); - } - } -}; - -// DEPRECATED -// TODO: switch return back to widget declaration at top of file when this is removed -if ( $.uiBackCompat !== false ) { - - // Backcompat for activeClass and hoverClass options - $.widget( "ui.droppable", $.ui.droppable, { - options: { - hoverClass: false, - activeClass: false - }, - _addActiveClass: function() { - this._super(); - if ( this.options.activeClass ) { - this.element.addClass( this.options.activeClass ); - } - }, - _removeActiveClass: function() { - this._super(); - if ( this.options.activeClass ) { - this.element.removeClass( this.options.activeClass ); - } - }, - _addHoverClass: function() { - this._super(); - if ( this.options.hoverClass ) { - this.element.addClass( this.options.hoverClass ); - } - }, - _removeHoverClass: function() { - this._super(); - if ( this.options.hoverClass ) { - this.element.removeClass( this.options.hoverClass ); - } - } - } ); -} - -var widgetsDroppable = $.ui.droppable; - /*! * jQuery UI Menu 1.12.1 diff --git a/addons/web/static/src/core/utils/draggable_hook_builder.js b/addons/web/static/src/core/utils/draggable_hook_builder.js index d7ab14638a2..b7362cdb99e 100644 --- a/addons/web/static/src/core/utils/draggable_hook_builder.js +++ b/addons/web/static/src/core/utils/draggable_hook_builder.js @@ -94,6 +94,7 @@ const DEFAULT_ACCEPTED_PARAMS = { edgeScrolling: [Object, Function], delay: [Number], tolerance: [Number], + iframeWindow: [Object, Function], }; const DEFAULT_DEFAULT_PARAMS = { elements: `.${DRAGGABLE_CLASS}`, @@ -231,7 +232,7 @@ function makeDOMHelpers(cleanup) { if (!el || !classNames.length) { return; } - cleanup.add(saveAttribute(el, "class")); + cleanup.add(() => el.classList.remove(...classNames)); el.classList.add(...classNames); }; @@ -552,6 +553,11 @@ export function makeDraggableHook(hookParams) { } dom.addClass(document.body, "pe-none", "user-select-none"); + if (params.iframeWindow) { + dom.addClass(params.iframeWindow.body, "pe-none", "user-select-none"); + } + // FIXME: adding pe-none and cursor on the same element makes + // no sense as pe-none prevents the cursor to be displayed. if (ctx.cursor) { dom.addStyle(document.body, { cursor: ctx.cursor }); } @@ -861,6 +867,12 @@ export function makeDraggableHook(hookParams) { passive: false, noAddedStyle: true, }); + if (params.iframeWindow) { + dom.addListener(params.iframeWindow, "touchmove", safePrevent, { + passive: false, + noAddedStyle: true, + }); + } } }; @@ -987,14 +999,20 @@ export function makeDraggableHook(hookParams) { }, () => [ctx.ref.el] ); + const addWindowListener = (type, listener, options) => { + if (params.iframeWindow) { + setupHooks.addListener(params.iframeWindow, type, listener, options); + } + setupHooks.addListener(window, type, listener, options); + }; // Other global event listeners. const throttledOnPointerMove = setupHooks.throttle(onPointerMove); - setupHooks.addListener(window, "pointermove", throttledOnPointerMove, { + addWindowListener("pointermove", throttledOnPointerMove, { passive: false, }); - setupHooks.addListener(window, "pointerup", onPointerUp); - setupHooks.addListener(window, "pointercancel", onPointerCancel); - setupHooks.addListener(window, "keydown", onKeyDown, { capture: true }); + addWindowListener("pointerup", onPointerUp); + addWindowListener("pointercancel", onPointerCancel); + addWindowListener("keydown", onKeyDown, { capture: true }); setupHooks.teardown(() => dragEnd(null)); return state; diff --git a/addons/web_editor/__manifest__.py b/addons/web_editor/__manifest__.py index 32a5e67b3cb..122a9fd1c32 100644 --- a/addons/web_editor/__manifest__.py +++ b/addons/web_editor/__manifest__.py @@ -127,7 +127,7 @@ Odoo Web Editor widget. 'web_editor/static/src/js/editor/odoo-editor/src/commands/toggleList.js', # utils - 'web_editor/static/src/js/editor/smooth_scroll_on_drag.js', + 'web_editor/static/src/js/editor/drag_and_drop.js', 'web_editor/static/src/js/wysiwyg/linkDialogCommand.js', 'web_editor/static/src/js/wysiwyg/MoveNodePlugin.js', 'web_editor/static/src/js/wysiwyg/PeerToPeer.js', diff --git a/addons/web_editor/static/src/js/editor/drag_and_drop.js b/addons/web_editor/static/src/js/editor/drag_and_drop.js new file mode 100644 index 00000000000..40738411f21 --- /dev/null +++ b/addons/web_editor/static/src/js/editor/drag_and_drop.js @@ -0,0 +1,201 @@ +/** @odoo-module **/ +import { makeDraggableHook } from "@web/core/utils/draggable_hook_builder"; +import { pick } from "@web/core/utils/objects"; +import { reactive } from "@odoo/owl"; +import { throttleForAnimation } from "@web/core/utils/timing"; +import { touching } from "@web/core/utils/ui"; + +/** @typedef {import("@web/core/utils/draggable_hook_builder").DraggableHandlerParams} DraggableHandlerParams */ +/** @typedef {import("@web/core/utils/draggable_hook_builder").DraggableBuilderParams} DraggableBuilderParams */ +/** @typedef {import("@web/core/utils/draggable").DraggableParams} DraggableParams */ + +/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement | null, helper: HTMLElement }} DragAndDropHandlerParams */ +/** @typedef {DraggableHandlerParams & { helper: HTMLElement }} DragAndDropStartParams */ +/** @typedef {DraggableHandlerParams & { dropzone: HTMLElement }} DropzoneHandlerParams */ +/** + * @typedef DragAndDropParams + * @extends {DraggableParams} + * + * MANDATORY + * @property {(() => Array)} dropzones a function that returns the available dropzones + * @property {(() => HTMLElement)} helper a function that returns a helper element + * that will follow the cursor when dragging + * @property {HTMLElement || (() => HTMLElement)} scrollingElement the element on + * which a scroll should be triggered + * + * HANDLERS (Optional) + * @property {(params: DragAndDropStartParams) => any} [onDragStart] + * called when a dragging sequence is initiated + * @property {(params: DropzoneHandlerParams) => any} [dropzoneOver] + * called when an element is over a dropzone + * @property {(params: DropzoneHandlerParams) => any} [dropzoneOut] + * called when an element is leaving a dropzone + * @property {(params: DragAndDropHandlerParams) => any} [onDrag] + * called when an element is being dragged + * @property {(params: DragAndDropHandlerParams) => any} [onDragEnd] + * called when the dragging sequence is over + */ +/** + * @typedef NativeDraggableState + * @property {(params: DraggableParams) => any} update + * method to update the params of the draggable + * @property {import("@web/core/utils/draggable").DraggableState} state + * state of the draggable component + * @property {() => any} destroy + * method to destroy and unbind the draggable component + */ +/** + * Utility function to create a native draggable component + * + * @param {DraggableBuilderParams} hookParams + * @param {DraggableParams} initialParams + * @returns {NativeDraggableState} + */ +export function useNativeDraggable(hookParams, initialParams) { + const setupFunctions = new Map(); + const cleanupFunctions = []; + const currentParams = { ...initialParams }; + const setupHooks = { + wrapState: reactive, + throttle: throttleForAnimation, + addListener: (el, type, callback, options) => { + el.addEventListener(type, callback, options); + cleanupFunctions.push(() => el.removeEventListener(type, callback)); + }, + setup: (setupFn, depsFn) => setupFunctions.set(setupFn, depsFn), + teardown: (cleanupFn) => { + cleanupFunctions.push(cleanupFn); + } + }; + // Compatibility for tests + const el = initialParams.ref.el; + el.classList.add("o_draggable"); + cleanupFunctions.push(() => el.classList.remove("o_draggable")); + + const draggableState = makeDraggableHook({ setupHooks, ...hookParams})(currentParams); + draggableState.enable = true; + const draggableComponent = { + state: draggableState, + update: (newParams) => { + Object.assign(currentParams, newParams); + setupFunctions.forEach((depsFn, setupFn) => setupFn(...depsFn())); + }, + destroy: () => { + cleanupFunctions.forEach((cleanupFn) => cleanupFn()); + } + }; + draggableComponent.update({}); + return draggableComponent; +} + +function updateElementPosition(el, { x, y }, styleFn, offset = { x: 0, y: 0 }) { + return styleFn(el, { top: `${y - offset.y}px`, left: `${x - offset.x}px`}); +} +/** @type DraggableBuilderParams */ +const dragAndDropHookParams = { + name: "useDragAndDrop", + acceptedParams: { + dropzones: [Function], + scrollingElement: [Object, Function], + helper: [Function], + extraWindow: [Object, Function], + }, + edgeScrolling: { enabled: true }, + onComputeParams({ ctx, params }) { + // The helper is mandatory and will follow the cursor instead + ctx.followCursor = false; + ctx.scrollingElement = params.scrollingElement; + ctx.getHelper = params.helper; + ctx.getDropZones = params.dropzones; + }, + onWillStartDrag: ({ ctx }) => { + ctx.current.container = ctx.scrollingElement; + ctx.current.helperOffset = { x: 0, y: 0 }; + }, + onDragStart: ({ ctx, addStyle, addCleanup }) => { + // Use the helper as the tracking element to properly update scroll values. + ctx.current.element = ctx.getHelper({ ...ctx.current, ...ctx.pointer }); + ctx.current.helper = ctx.current.element; + ctx.current.helper.style.position = "fixed"; + // We want the pointer events on the helper so that the cursor + // is properly displayed. + ctx.current.helper.classList.remove("o_dragged"); + ctx.current.helper.style.cursor = ctx.cursor; + ctx.current.helper.style.pointerEvents = "auto"; + + // If the helper is inside the iframe, we want pointer events on the + // frame element so that they reach the window and properly apply + // the cursor. + const frameElement = ctx.current.helper.ownerDocument.defaultView.frameElement; + if (frameElement) { + addStyle(frameElement, { pointerEvents: "auto" }); + } + + addCleanup(() => ctx.current.helper.remove()); + + updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset); + + return pick(ctx.current, "element", "helper"); + }, + onDrag: ({ ctx, addStyle, callHandler }) => { + ctx.current.helper.classList.add("o_draggable_dragging"); + + updateElementPosition(ctx.current.helper, ctx.pointer, addStyle, ctx.current.helperOffset); + // Unfortunately, DOMRect is not an Object, so spreading operator from + // `touching` does not work, so convert DOMRect to plain object. + let helperRect = ctx.current.helper.getBoundingClientRect(); + helperRect = { + x: helperRect.x, + y: helperRect.y, + width: helperRect.width, + height: helperRect.height, + }; + const dropzoneEl = touching(ctx.getDropZones(), helperRect)[0]; + // Update the drop zone if it's in grid mode + if (ctx.current.dropzone?.el && ctx.current.dropzone.el.classList.contains("oe_grid_zone")) { + ctx.current.dropzone.rect = ctx.current.dropzone.el.getBoundingClientRect(); + } + if ( + ctx.current.dropzone && + ( + ctx.current.dropzone.el === dropzoneEl || + touching([ctx.current.helper], ctx.current.dropzone.rect).length > 0 + ) + ) { + // If no new dropzone but old one is still valid, return early. + return pick(ctx.current, "element", "dropzone", "helper"); + } + + if (ctx.current.dropzone && dropzoneEl !== ctx.current.dropzone.el) { + callHandler("dropzoneOut", { dropzone: ctx.current.dropzone }); + delete ctx.current.dropzone; + } + + if (dropzoneEl) { + // Save rect information prior to calling the over function + // to keep a consistent dropzone even if content was added. + const rect = DOMRect.fromRect(dropzoneEl.getBoundingClientRect()); + ctx.current.dropzone = { + el: dropzoneEl, + rect: { + x: rect.x, y: rect.y, width: rect.width, height: rect.height + } + }; + callHandler("dropzoneOver", { dropzone: ctx.current.dropzone }); + } + return pick(ctx.current, "element", "dropzone", "helper"); + }, + onDragEnd({ ctx }) { + return pick(ctx.current, "element", "dropzone", "helper"); + } +}; +/** + * Function to start a drag and drop handler + * + * @param {DragAndDropParams} initialParams params given to the drag and drop + * component + * @returns {NativeDraggableState} + */ +export function useDragAndDrop(initialParams) { + return useNativeDraggable(dragAndDropHookParams, initialParams); +} diff --git a/addons/web_editor/static/src/js/editor/smooth_scroll_on_drag.js b/addons/web_editor/static/src/js/editor/smooth_scroll_on_drag.js deleted file mode 100644 index 7c4c3ab109e..00000000000 --- a/addons/web_editor/static/src/js/editor/smooth_scroll_on_drag.js +++ /dev/null @@ -1,420 +0,0 @@ -/** @odoo-module **/ - -/** - * Provides a helper for SmoothScrollOnDrag options.offsetElements - */ -class OffsetElementsHelper { - /** - * @constructor - * @param {Object} offsetElements - * @param {jQuery} [offsetElements.$top] top offset element - * @param {jQuery} [offsetElements.$right] right offset element - * @param {jQuery} [offsetElements.$bottom] bottom offset element - * @param {jQuery} [offsetElements.$left] left offset element - */ - constructor(offsetElements) { - this.offsetElements = offsetElements; - } - top () { - if (!this.offsetElements.$top || !this.offsetElements.$top.length) { - return 0; - } - return this.offsetElements.$top.get(0).getBoundingClientRect().bottom; - } - right () { - if (!this.offsetElements.$right || !this.offsetElements.$right.length) { - return 0; - } - return this.offsetElements.$right.get(0).getBoundingClientRect().left; - } - bottom () { - if (!this.offsetElements.$bottom || !this.offsetElements.$bottom.length) { - return 0; - } - return this.offsetElements.$bottom.get(0).getBoundingClientRect().top; - } - left () { - if (!this.offsetElements.$left || !this.offsetElements.$left.length) { - return 0; - } - return this.offsetElements.$left.get(0).getBoundingClientRect().right; - } -} - -/** - * Provides smooth scroll behaviour on drag. - */ -export class SmoothScrollOnDrag { - - /** - * @constructor - * @param {Object} parent The parent widget that uses this class. - * @param {jQuery} $element The element the smooth scroll on drag has to be set on. - * @param {jQuery} $scrollTarget The element the scroll will be triggered on. - * @param {Object} [options={}] - * @param {Object} [options.jQueryDraggableOptions={}] The configuration to be passed to - * the jQuery draggable function (all will be passed except scroll which will - * be overridden to false). - * @param {Number} [options.scrollOffsetThreshold=150] (Integer) The distance from the - * bottom/top of the options.$scrollTarget from which the smooth scroll will be - * triggered. - * @param {Number} [options.scrollStep=20] (Integer) The step of the scroll. - * @param {Number} [options.scrollTimerInterval=5] (Integer) The interval (in ms) the - * scrollStep will be applied. - * @param {Object} [options.scrollBoundaries = {}] Specifies whether scroll can still be triggered - * when dragging $element outside of target. - * @param {Object} [options.scrollBoundaries.top = true] Specifies whether scroll can still be triggered - * when dragging $element above the top edge of target. - * @param {Object} [options.scrollBoundaries.right = true] Specifies whether scroll can still be triggered - * when dragging $element after the right edge of target. - * @param {Object} [options.scrollBoundaries.bottom = true] Specifies whether scroll can still be triggered - * when dragging $element bellow the bottom edge of target. - * @param {Object} [options.scrollBoundaries.left = true] Specifies whether scroll can still be triggered - * when dragging $element before the left edge of target. - * @param {Object} [options.offsetElements={}] Visible elements in $scrollTarget that - * reduce $scrollTarget drag visible area (scroll will be triggered sooner than - * normally). A selector is passed so that elements such as automatically hidden - * menu can then be correctly handled. - * @param {jQuery} [options.offsetElements.$top] Visible top offset element which height will - * be taken into account when triggering scroll at the top of the $scrollTarget. - * @param {jQuery} [options.offsetElements.$right] Visible right offset element which width - * will be taken into account when triggering scroll at the right side of the - * $scrollTarget. - * @param {jQuery} [options.offsetElements.$bottom] Visible bottom offset element which height - * will be taken into account when triggering scroll at bottom of the $scrollTarget. - * @param {jQuery} [options.offsetElements.$left] Visible left offset element which width - * will be taken into account when triggering scroll at the left side of the - * $scrollTarget. - * @param {boolean} [options.disableHorizontalScroll = false] Disable horizontal scroll if not needed. - */ - constructor($element, $scrollTarget, options = {}) { - this.$element = $element; - this.$scrollTarget = $scrollTarget; - this.options = options; - this.targetWindow = this.$element[0].ownerDocument.defaultView; - const insideIframe = this.targetWindow !== window.top; - - // Setting optional options to their default value if not provided - this.options.jQueryDraggableOptions = this.options.jQueryDraggableOptions || {}; - if (!this.options.jQueryDraggableOptions.cursorAt) { - this.$element.on('mousedown.smooth_scroll', this._onElementMouseDown.bind(this)); - } - this.options.scrollOffsetThreshold = this.options.scrollOffsetThreshold || 150; - this.options.scrollStep = this.options.scrollStep || 20; - this.options.scrollTimerInterval = this.options.scrollTimerInterval || 5; - this.options.offsetElements = this.options.offsetElements || {}; - this.options.offsetElementsManager = new OffsetElementsHelper(this.options.offsetElements); - this.options.scrollBoundaries = Object.assign({ - top: true, - right: true, - bottom: true, - left: true - }, this.options.scrollBoundaries); - - this.autoScrollHandler = null; - - this.scrollStepDirectionEnum = { - up: -1, - right: 1, - down: 1, - left: -1, - }; - - this.options.jQueryDraggableOptions.scroll = false; - this.options.disableHorizontalScroll = this.options.disableHorizontalScroll || false; - const draggableOptions = Object.assign({}, this.options.jQueryDraggableOptions, { - start: (ev, ui) => { - this._onSmoothDragStart(ev, ui, this.options.jQueryDraggableOptions.start); - if (insideIframe) { - this.onParentWindowMouseup = this._onParentWindowMouseup.bind(this); - window.top.addEventListener('mouseup', this.onParentWindowMouseup, {once: true}); - } - }, - drag: (ev, ui) => this._onSmoothDrag(ev, ui, this.options.jQueryDraggableOptions.drag), - stop: (ev, ui) => { - if (insideIframe) { - window.top.removeEventListener('mouseup', this.onParentWindowMouseup, {once: true}); - } - this._onSmoothDragStop(ev, ui, this.options.jQueryDraggableOptions.stop); - } - }); - this.$element.draggable(draggableOptions); - } - /** - * @override - */ - destroy() { - this.$element.off('.smooth_scroll'); - this._stopSmoothScroll(); - } - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * Starts the scroll process using the options. - * The options will be updated dynamically when the handler _onSmoothDrag - * will be called. The interval will be cleared when the handler - * _onSmoothDragStop will be called. - * - * @private - * @param {Object} ui The jQuery drag handler ui parameter. - */ - _startSmoothScroll(ui) { - this._stopSmoothScroll(); - - this._initialScrollBehavior = this.$scrollTarget[0].style.scrollBehavior; - this.$scrollTarget[0].style.scrollBehavior = 'auto'; - - this.autoScrollHandler = setInterval( - () => { - // Prevents Delta's from being different from 0 when scroll should not occur (except when - // helper is dragged outside of this.$scrollTarget's visible area as it increases - // this.$scrollTarget's scrollHeight). - // Also, this code prevents the helper from being incorrectly repositioned when target is - // a child of this.$scrollTarget. - this.verticalDelta = Math.min( - // Ensures scrolling stops when dragging bellow this.$scrollTarget bottom. - Math.max( - 0, - this.$scrollTarget.get(0).scrollHeight - - (this.$scrollTarget.scrollTop() + this.$scrollTarget.innerHeight()) - ), - // Ensures scrolling stops when dragging above this.$scrollTarget top. - Math.max( - this.verticalDelta, - -this.$scrollTarget.scrollTop() - ) - ); - this.horizontalDelta = Math.min( - //Ensures scrolling stops when dragging left to this.$scrollTarget. - Math.max( - 0, - this.$scrollTarget.get(0).scrollWidth - - (this.$scrollTarget.scrollLeft() + this.$scrollTarget.innerWidth()) - ), - //Ensures scrolling stops when dragging right to this.$scrollTarget. - Math.max( - this.horizontalDelta, - -this.$scrollTarget.scrollLeft() - ) - ); - - // Keep helper at right position while scrolling when helper is a child of this.$scrollTarget. - if (this.scrollTargetIsParent) { - const offset = ui.helper.offset(); - ui.helper.offset({ - top: offset.top + this.verticalDelta, - left: offset.left + this.horizontalDelta - }); - } - this.$scrollTarget.scrollTop( - this.$scrollTarget.scrollTop() + - this.verticalDelta - ); - if (!this.options.disableHorizontalScroll) { - this.$scrollTarget.scrollLeft( - this.$scrollTarget.scrollLeft() + - this.horizontalDelta - ); - } - }, - this.options.scrollTimerInterval - ); - } - /** - * Stops the scroll process if any is running. - * - * @private - */ - _stopSmoothScroll() { - clearInterval(this.autoScrollHandler); - - this.$scrollTarget[0].style.scrollBehavior = this._initialScrollBehavior || ''; - } - /** - * Updates the options depending on the offset position of the draggable - * helper. In the same time options are used by an interval to trigger - * scroll behaviour. - * @see {@link _startSmoothScroll} for interval implementation details. - * - * @private - * @param {Object} ui The jQuery drag handler ui parameter. - */ - _updatePositionOptions(ui) { - const draggableHelperOffset = ui.offset; - const scrollTargetOffset = this.$scrollTarget.offset(); - let visibleOffset = { - top: draggableHelperOffset.top - - scrollTargetOffset.top - + this.options.jQueryDraggableOptions.cursorAt.top - - this.options.offsetElementsManager.top(), - right: scrollTargetOffset.left + this.$scrollTarget.outerWidth() - - draggableHelperOffset.left - - this.options.jQueryDraggableOptions.cursorAt.left - - this.options.offsetElementsManager.right(), - bottom: scrollTargetOffset.top + this.$scrollTarget.outerHeight() - - draggableHelperOffset.top - - this.options.jQueryDraggableOptions.cursorAt.top - - this.options.offsetElementsManager.bottom(), - left: draggableHelperOffset.left - - scrollTargetOffset.left - + this.options.jQueryDraggableOptions.cursorAt.left - - this.options.offsetElementsManager.left(), - }; - - if (this.iframeOffset) { - const { x, y } = this.iframeOffset; - visibleOffset.left -= x; - visibleOffset.top -= y; - visibleOffset.bottom += y; - visibleOffset.right += x; - } - - // If this.$scrollTarget is the html tag, we need to take the scroll - // position into account as offsets positions are calculated relative to - // the document (thus ). Except in the case where what is being - // dragged is out of an iframe and that the scrolling is being done - // inside that iframe. - if (this.scrollTargetIsDocument && !this.iframeOffset) { - const scrollTargetScrollTop = this.$scrollTarget.scrollTop(); - const scrollTargetScrollLeft = this.$scrollTarget.scrollLeft(); - visibleOffset.top -= scrollTargetScrollTop; - visibleOffset.right += scrollTargetScrollLeft; - visibleOffset.bottom += scrollTargetScrollTop; - visibleOffset.left -= scrollTargetScrollLeft; - } - - const scrollDecelerator = { - vertical: 0, - horizontal: 0, - }; - - const scrollStepDirection = { - vertical: this.scrollStepDirectionEnum.down, - horizontal: this.scrollStepDirectionEnum.right, - }; - - // Prevent scroll if outside of scroll boundaries - if ((!this.options.scrollBoundaries.top && visibleOffset.top < 0) || - (!this.options.scrollBoundaries.right && visibleOffset.right < 0) || - (!this.options.scrollBoundaries.bottom && visibleOffset.bottom < 0) || - (!this.options.scrollBoundaries.left && visibleOffset.left < 0)) { - scrollDecelerator.horizontal = 1; - scrollDecelerator.vertical = 1; - } else { - // Manage vertical scroll - if (visibleOffset.bottom <= this.options.scrollOffsetThreshold) { - scrollDecelerator.vertical = Math.max(0, visibleOffset.bottom) - / this.options.scrollOffsetThreshold; - } else if (visibleOffset.top <= this.options.scrollOffsetThreshold) { - scrollDecelerator.vertical = Math.max(0, visibleOffset.top) - / this.options.scrollOffsetThreshold; - scrollStepDirection.vertical = this.scrollStepDirectionEnum.up; - } else { - scrollDecelerator.vertical = 1; - } - - // Manage horizontal scroll - if (visibleOffset.right <= this.options.scrollOffsetThreshold) { - scrollDecelerator.horizontal = Math.max(0, visibleOffset.right) - / this.options.scrollOffsetThreshold; - } else if (visibleOffset.left <= this.options.scrollOffsetThreshold) { - scrollDecelerator.horizontal = Math.max(0, visibleOffset.left) - / this.options.scrollOffsetThreshold; - scrollStepDirection.horizontal = this.scrollStepDirectionEnum.left; - } else { - scrollDecelerator.horizontal = 1; - } - } - - this.verticalDelta = Math.ceil(scrollStepDirection.vertical * - this.options.scrollStep * - (1 - Math.sqrt(scrollDecelerator.vertical))); - this.horizontalDelta = Math.ceil(scrollStepDirection.horizontal * - this.options.scrollStep * - (1 - Math.sqrt(scrollDecelerator.horizontal))); - } - - //-------------------------------------------------------------------------- - // Handlers - //-------------------------------------------------------------------------- - - /** - * Called when mouse button is down on this.$element. - * Updates the mouse cursor position variable. - * - * @private - * @param {Object} ev The jQuery mousedown handler event parameter. - */ - _onElementMouseDown(ev) { - const elementOffset = $(ev.target).offset(); - this.options.jQueryDraggableOptions.cursorAt = { - top: ev.pageY - elementOffset.top, - left: ev.pageX - elementOffset.left, - }; - } - /** - * Called when dragging the element. - * Updates the position options and call the provided callback if any. - * - * @private - * @param {Object} ev The jQuery drag handler event parameter. - * @param {Object} ui The jQuery drag handler ui parameter. - * @param {Function} onDragCallback The jQuery drag callback. - */ - _onSmoothDrag(ev, ui, onDragCallback) { - this._updatePositionOptions(ui); - if (typeof onDragCallback === 'function') { - onDragCallback.call(ui.helper, ev, ui); - } - } - /** - * Called when starting to drag the element. - * Updates the position params, starts smooth scrolling process and call the - * provided callback if any. - * - * @private - * @param {Object} ev The jQuery drag handler event parameter. - * @param {Object} ui The jQuery drag handler ui parameter. - * @param {Function} onDragStartCallBack The jQuery drag callback. - */ - _onSmoothDragStart(ev, ui, onDragStartCallBack) { - this.scrollTargetIsDocument = this.$scrollTarget.is('html'); - this.iframeOffset = false; - this.scrollTargetIsParent = this.$scrollTarget.get(0).contains(this.$element.get(0)); - this._updatePositionOptions(ui); - this._startSmoothScroll(ui); - if (typeof onDragStartCallBack === 'function') { - onDragStartCallBack.call(ui.helper, ev, ui); - } - } - /** - * Called when stopping to drag the element. - * Stops the smooth scrolling process and call the provided callback if any. - * - * @private - * @param {Object} ev The jQuery drag handler event parameter. - * @param {Object} ui The jQuery drag handler ui parameter. - * @param {Function} onDragEndCallBack The jQuery drag callback. - */ - _onSmoothDragStop(ev, ui, onDragEndCallBack) { - this._stopSmoothScroll(); - if (typeof onDragEndCallBack === 'function') { - onDragEndCallBack.call(ui.helper, ev, ui); - } - } - /** - * Called when the mouse is released outside the page iframe (e.g. the - * editor panel in Website). This is only useful in Chrome, where the 'stop' - * event of jQuery Draggable does not trigger a 'mouseup' event outside of - * the "preview" page iframe. - * - * @private - */ - _onParentWindowMouseup() { - this.targetWindow.document.dispatchEvent(new Event('mouseup')); - } -} - diff --git a/addons/web_editor/static/src/js/editor/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js index b787d32cb04..6d4742608bd 100644 --- a/addons/web_editor/static/src/js/editor/snippets.editor.js +++ b/addons/web_editor/static/src/js/editor/snippets.editor.js @@ -6,8 +6,8 @@ import core from "@web/legacy/js/services/core"; import Dialog from "@web/legacy/js/core/dialog"; import dom from "@web/legacy/js/core/dom"; import Widget from "@web/legacy/js/core/widget"; +import { useDragAndDrop } from "@web_editor/js/editor/drag_and_drop"; import options from "@web_editor/js/editor/snippets.options"; -import { SmoothScrollOnDrag } from "@web_editor/js/editor/smooth_scroll_on_drag"; import weUtils from "@web_editor/js/common/utils"; import * as gridUtils from "@web_editor/js/common/grid_layout_utils"; import { escape } from "@web/core/utils/strings"; @@ -113,10 +113,11 @@ var SnippetEditor = Widget.extend({ // Initialize move/clone/remove buttons if (this.isTargetMovable) { this.dropped = false; - this.draggableComponent = this._initSmoothScrollOnDrag(".o_move_handle", this.$el); + this.draggableComponent = this._initDragAndDrop(".o_move_handle", ".oe_overlay", this.el); if (!this.$target[0].matches("section")) { // Allow the user to drag the image itself to move the target. - this.draggableComponentImgs = this._initSmoothScrollOnDrag("img", this.$target); + this.$target[0].classList.add("o_draggable"); + this.draggableComponentImgs = this._initDragAndDrop("img", ".o_draggable", this.$target[0]); } } else { this.$('.o_overlay_move_options').addClass('d-none'); @@ -821,20 +822,26 @@ var SnippetEditor = Widget.extend({ }); }, /** - * Creates a SmoothScrollOnDrag object. + * Initialize drag and drop handlers. * * @private * @param {String} handle css selector for grabble element - * @param {jQuery} $element the smooth scroll on drag has to be set on - * @returns {SmoothScrollOnDrag} + * @param {String} elementsSelector selector for elements that will be dragged. + * @param {HTMLElement} element element to listen for drag events. + * @returns {Object} the drag state. */ - _initSmoothScrollOnDrag(handle, $element) { - const jQueryDraggableOptions = { - cursorAt: { - left: 10, - top: 10 - }, + _initDragAndDrop(handle, elementsSelector, element) { + const modalAncestorEl = this.$target[0].closest('.modal'); + const $scrollable = modalAncestorEl && $(modalAncestorEl) + || (this.options.wysiwyg.snippetsMenu && this.options.wysiwyg.snippetsMenu.$scrollable) + || (this.$scrollingElement.length && this.$scrollingElement) + || $().getScrollingElement(this.ownerDocument); + const dragAndDropOptions = { + ref: { el: element }, + elements: elementsSelector, handle: handle, + scrollingElement: $scrollable[0], + enable: () => !!this.$el.find('.o_move_handle:visible').length || this.dragStarted, helper: () => { const cloneEl = this.$el[0].cloneNode(true); cloneEl.style.width = "24px"; @@ -842,18 +849,17 @@ var SnippetEditor = Widget.extend({ cloneEl.style.border = "0"; this.$el[0].ownerDocument.body.appendChild(cloneEl); cloneEl.classList.remove("d-none"); - return $(cloneEl); + cloneEl.classList.remove("o_dragged"); + return cloneEl; }, - start: (ev, ui) => { - if (!this.$el.find('.o_move_handle:visible').length) { - return false; - } + onDragStart: (args) => { this.dragStarted = true; - this.mousePositionYOnElement = ev.originalEvent.clientY - ui.offset.top; - this.mousePositionXOnElement = ev.originalEvent.clientX - ui.offset.left; - this._onDragAndDropStart(); + const targetRect = this.$target[0].getBoundingClientRect(); + this.mousePositionYOnElement = args.y - targetRect.y; + this.mousePositionXOnElement = args.x - targetRect.x; + this._onDragAndDropStart(args); }, - stop: (...args) => { + onDragEnd: (...args) => { if (!this.dragStarted) { return false; } @@ -866,16 +872,13 @@ var SnippetEditor = Widget.extend({ this._onDragAndDropStop(...args); }, 0); }, - refreshPositions: true, // So the dropzone expands when its size increases. + onDrag: this._onDragMove.bind(this), + dropzoneOver: this.dropzoneOver.bind(this), + dropzoneOut: this.dropzoneOut.bind(this), + dropzones: () => this.$dropZones?.toArray() || [], }; - const scrollOptions = - this.options.getScrollOptions({ jQueryDraggableOptions: jQueryDraggableOptions }); - const modalAncestorEl = this.$target[0].closest('.modal'); - const $scrollable = modalAncestorEl && $(modalAncestorEl) - || (this.options.wysiwyg.snippetsMenu && this.options.wysiwyg.snippetsMenu.$scrollable) - || (this.$scrollingElement.length && this.$scrollingElement) - || $().getScrollingElement(this.ownerDocument); - return new SmoothScrollOnDrag($element, $scrollable, scrollOptions); + const finalOptions = this.options.getDragAndDropOptions(dragAndDropOptions); + return useDragAndDrop(finalOptions); }, /** * @private @@ -935,7 +938,7 @@ var SnippetEditor = Widget.extend({ const rowEl = previousDropzoneEl.parentNode; if (rowEl.classList.contains('o_grid_mode')) { - self.$body[0].removeEventListener('mousemove', self.onDragMove, false); + self.dragState.gridMode = false; const fromGridToGrid = currentDropzoneEl.classList.contains('oe_grid_zone'); if (fromGridToGrid) { // If we went from a grid dropzone to an other grid one. @@ -989,7 +992,15 @@ var SnippetEditor = Widget.extend({ * * @private */ - _onDragAndDropStart: function () { + _onDragAndDropStart({ helper, addStyle }) { + // If the helper is inside the iframe, we want pointer events on the + // frame element so that they reach the window and properly apply + // the cursor. + const frameElement = helper.ownerDocument.defaultView.frameElement; + if (frameElement) { + addStyle(frameElement, { pointerEvents: "auto" }); + } + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); this.trigger_up('drag_and_drop_start'); this.options.wysiwyg.odooEditor.automaticStepUnactive(); @@ -1018,8 +1029,6 @@ var SnippetEditor = Widget.extend({ const allowGridMode = hasGridLayoutOption || rowEl.classList.contains('o_grid_mode'); // Number of grid columns and rows in the grid item (BS column). - let columnColCount; - let columnRowCount; if (rowEl.classList.contains('row') && this.options.isWebsite) { if (allowGridMode) { // Toggle grid mode if it is not already on. @@ -1037,10 +1046,8 @@ var SnippetEditor = Widget.extend({ const rowStart = self.$target[0].style.gridRowStart; const rowEnd = self.$target[0].style.gridRowEnd; - columnColCount = columnEnd - columnStart; - columnRowCount = rowEnd - rowStart; - this.dragState.columnColCount = columnColCount; - this.dragState.columnRowCount = columnRowCount; + this.dragState.columnColCount = columnEnd - columnStart; + this.dragState.columnRowCount = rowEnd - rowStart; // Storing the current grid and grid area to use them for the // history. @@ -1173,156 +1180,146 @@ var SnippetEditor = Widget.extend({ } else if (canBeSanitizedUnless === 'form') { this.$dropZones = this.$dropZones.not('[data-oe-sanitize][data-oe-sanitize!="allow_form"] .oe_drop_zone'); } - this.$dropZones.droppable({ - over: function () { - if (self.dropped) { - self.$target.detach(); - } + }, + dropzoneOver({ dropzone }) { + if (this.dropped) { + this.$target.detach(); + } - // Prevent a column to be trapped in an upper grid dropzone at - // the start of the drag. - if (self.dragState.isColumn && self.dragState.overFirstDropzone) { - self.dragState.overFirstDropzone = false; + // Prevent a column to be trapped in an upper grid dropzone at + // the start of the drag. + if (this.dragState.isColumn && this.dragState.overFirstDropzone) { + this.dragState.overFirstDropzone = false; - // The column is considered as glued to the dropzone if the - // dropzone is above and if the space between them is less - // than 25px (the move handle height is 22px so 25 is a - // safety margin). - const columnTop = self.dragState.columnTop; - const dropzoneBottom = this.getBoundingClientRect().bottom; - const areDropzonesGlued = (columnTop >= dropzoneBottom) && (columnTop - dropzoneBottom < 25); + // The column is considered as glued to the dropzone if the + // dropzone is above and if the space between them is less + // than 25px (the move handle height is 22px so 25 is a + // safety margin). + const columnTop = this.dragState.columnTop; + const dropzoneBottom = dropzone.el.getBoundingClientRect().bottom; + const areDropzonesGlued = (columnTop >= dropzoneBottom) && (columnTop - dropzoneBottom < 25); - if (areDropzonesGlued && this.classList.contains('oe_grid_zone')) { - return; - } - } + if (areDropzonesGlued && dropzone.el.classList.contains('oe_grid_zone')) { + return; + } + } - self.dropped = true; - const $dropzone = $(this).first().after(self.$target); - $dropzone.addClass('invisible'); + this.dropped = true; + const $dropzone = $(dropzone.el).first().after(this.$target); + $dropzone.addClass('invisible'); - // Checking if the "out" event happened before this "over": if - // `self.dragState.currentDropzoneEl` exists, "out" didn't - // happen because it deletes it. We are therefore in the case - // of an "over" after an "over" and we need to escape the - // previous dropzone first. - if (self.dragState.currentDropzoneEl) { - self._outPreviousDropzone.apply(self.dragState.currentDropzoneEl, [self, $dropzone[0]]); - } - self.dragState.currentDropzoneEl = $dropzone[0]; + // Checking if the "out" event happened before dropzone.el "over": if + // `this.dragState.currentDropzoneEl` exists, "out" didn't + // happen because it deletes it. We are therefore in the case + // of an "over" after an "over" and we need to escape the + // previous dropzone first. + if (this.dragState.currentDropzoneEl) { + this._outPreviousDropzone.apply(this.dragState.currentDropzoneEl, [this, $dropzone[0]]); + } + this.dragState.currentDropzoneEl = $dropzone[0]; - if ($dropzone[0].classList.contains('oe_grid_zone')) { - // Case where the column we are dragging is over a grid - // dropzone. - const rowEl = $dropzone[0].parentNode; + if ($dropzone[0].classList.contains('oe_grid_zone')) { + // Case where the column we are dragging is over a grid + // dropzone. + const rowEl = $dropzone[0].parentNode; - // If the column doesn't come from a grid mode snippet. - if (!self.$target[0].classList.contains('o_grid_item')) { - // Converting the column to grid. - self.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); - const spans = gridUtils._convertColumnToGrid(rowEl, self.$target[0], self.dragState.columnWidth, self.dragState.columnHeight); - self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); - columnColCount = spans.columnColCount; - columnRowCount = spans.columnRowCount; + // If the column doesn't come from a grid mode snippet. + if (!this.$target[0].classList.contains('o_grid_item')) { + // Converting the column to grid. + this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); + const spans = gridUtils._convertColumnToGrid(rowEl, this.$target[0], this.dragState.columnWidth, this.dragState.columnHeight); + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); + this.dragState.columnColCount = spans.columnColCount; + this.dragState.columnRowCount = spans.columnRowCount; - // Storing the column spans. - self.dragState.columnColCount = columnColCount; - self.dragState.columnRowCount = columnRowCount; - } + // Storing the column spans. + } - // Creating the drag helper. - const dragHelperEl = document.createElement('div'); - dragHelperEl.classList.add('o_we_drag_helper'); - dragHelperEl.style.gridArea = `1 / 1 / ${1 + columnRowCount} / ${1 + columnColCount}`; - rowEl.append(dragHelperEl); + const columnColCount = this.dragState.columnColCount; + const columnRowCount = this.dragState.columnRowCount; + // Creating the drag helper. + const dragHelperEl = document.createElement('div'); + dragHelperEl.classList.add('o_we_drag_helper'); + dragHelperEl.style.gridArea = `1 / 1 / ${1 + columnRowCount} / ${1 + columnColCount}`; + rowEl.append(dragHelperEl); - // Creating the background grid and updating the dropzone - // (in the case where the column over the dropzone is - // bigger than the grid). - const backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, columnRowCount); - const rowCount = Math.max(rowEl.dataset.rowCount, columnRowCount); - $dropzone[0].style.gridRowEnd = rowCount + 1; + // Creating the background grid and updating the dropzone + // (in the case where the column over the dropzone is + // bigger than the grid). + const backgroundGridEl = gridUtils._addBackgroundGrid(rowEl, columnRowCount); + const rowCount = Math.max(rowEl.dataset.rowCount, columnRowCount); + $dropzone[0].style.gridRowEnd = rowCount + 1; - self.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); - // Setting the moving grid item, the background grid and - // the drag helper z-indexes. The grid item z-index is set - // to its original one if we are in its starting grid, or - // to the maximum z-index of the grid otherwise. - if (rowEl === self.dragState.startingGrid) { - self.$target[0].style.zIndex = self.dragState.startingZIndex; - } else { - gridUtils._setElementToMaxZindex(self.$target[0], rowEl); - } - gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl); - gridUtils._setElementToMaxZindex(dragHelperEl, rowEl); + this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); + // Setting the moving grid item, the background grid and + // the drag helper z-indexes. The grid item z-index is set + // to its original one if we are in its starting grid, or + // to the maximum z-index of the grid otherwise. + if (rowEl === this.dragState.startingGrid) { + this.$target[0].style.zIndex = this.dragState.startingZIndex; + } else { + gridUtils._setElementToMaxZindex(this.$target[0], rowEl); + } + gridUtils._setElementToMaxZindex(backgroundGridEl, rowEl); + gridUtils._setElementToMaxZindex(dragHelperEl, rowEl); - // Setting the column height and width to keep its size - // when the grid-area is removed (as it prevents it from - // moving with the mouse). - const gridProp = gridUtils._getGridProperties(rowEl); - const columnHeight = columnRowCount * (gridProp.rowSize + gridProp.rowGap) - gridProp.rowGap; - const columnWidth = columnColCount * (gridProp.columnSize + gridProp.columnGap) - gridProp.columnGap; - self.$target[0].style.height = columnHeight + 'px'; - self.$target[0].style.width = columnWidth + 'px'; - self.$target[0].style.position = 'absolute'; - self.$target[0].style.removeProperty('grid-area'); - rowEl.style.position = 'relative'; - self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); + // Setting the column height and width to keep its size + // when the grid-area is removed (as it prevents it from + // moving with the mouse). + const gridProp = gridUtils._getGridProperties(rowEl); + const columnHeight = columnRowCount * (gridProp.rowSize + gridProp.rowGap) - gridProp.rowGap; + const columnWidth = columnColCount * (gridProp.columnSize + gridProp.columnGap) - gridProp.columnGap; + this.$target[0].style.height = columnHeight + 'px'; + this.$target[0].style.width = columnWidth + 'px'; + this.$target[0].style.position = 'absolute'; + this.$target[0].style.removeProperty('grid-area'); + rowEl.style.position = 'relative'; + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); - // Storing useful information and adding an event listener. - self.dragState.startingHeight = rowEl.clientHeight; - self.dragState.currentHeight = rowEl.clientHeight; - self.dragState.dragHelperEl = dragHelperEl; - self.dragState.backgroundGridEl = backgroundGridEl; - self.onDragMove = self._onDragMove.bind(self); - self.$body[0].addEventListener('mousemove', self.onDragMove, false); - } - }, - out: function () { - const dropzoneEl = this; - const rowEl = dropzoneEl.parentNode; + // Storing useful information and adding an event listener. + this.dragState.startingHeight = rowEl.clientHeight; + this.dragState.currentHeight = rowEl.clientHeight; + this.dragState.dragHelperEl = dragHelperEl; + this.dragState.backgroundGridEl = backgroundGridEl; + this.dragState.gridMode = true; + } + }, + dropzoneOut({ dropzone }) { + const rowEl = dropzone.el.parentNode; - // Checking if the "out" event happens right after the "over" - // of the same dropzone. If it is not the case, we don't do - // anything since the previous dropzone was already escaped (at - // the start of the over). - const sameDropzoneAsCurrent = self.dragState.currentDropzoneEl === dropzoneEl; + // Checking if the "out" event happens right after the "over" + // of the same dropzone. If it is not the case, we don't do + // anything since the previous dropzone was already escaped (at + // the start of the over). + const sameDropzoneAsCurrent = this.dragState.currentDropzoneEl === dropzone.el; - if (sameDropzoneAsCurrent) { - if (rowEl.classList.contains('o_grid_mode')) { - // Removing the listener + cleaning. - self.$body[0].removeEventListener('mousemove', self.onDragMove, false); - gridUtils._gridCleanUp(rowEl, self.$target[0]); - self.$target[0].style.removeProperty('z-index'); + if (sameDropzoneAsCurrent) { + if (rowEl.classList.contains('o_grid_mode')) { + // Removing the listener + cleaning. + this.dragState.gridMode = false; + gridUtils._gridCleanUp(rowEl, this.$target[0]); + this.$target[0].style.removeProperty('z-index'); - // Removing the drag helper and the background grid and - // resizing the grid and the dropzone. - self.dragState.dragHelperEl.remove(); - self.dragState.backgroundGridEl.remove(); - self.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); - gridUtils._resizeGrid(rowEl); - self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); - const rowCount = parseInt(rowEl.dataset.rowCount); - dropzoneEl.style.gridRowEnd = Math.max(rowCount + 1, 1); - } + // Removing the drag helper and the background grid and + // resizing the grid and the dropzone. + this.dragState.dragHelperEl.remove(); + this.dragState.backgroundGridEl.remove(); + this.options.wysiwyg.odooEditor.observerActive('dragAndDropMoveSnippet'); + gridUtils._resizeGrid(rowEl); + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); + const rowCount = parseInt(rowEl.dataset.rowCount); + dropzone.el.style.gridRowEnd = Math.max(rowCount + 1, 1); + } - var prev = self.$target.prev(); - if (this === prev[0]) { - self.dropped = false; - self.$target.detach(); - $(this).removeClass('invisible'); - } + var prev = this.$target.prev(); + if (dropzone.el === prev[0]) { + this.dropped = false; + this.$target.detach(); + $(dropzone.el).removeClass('invisible'); + } - delete self.dragState.currentDropzoneEl; - } - }, - }); - - // Trigger a scroll on the draggable element so that jQuery updates - // the position of the drop zones. - self.draggableComponent.$scrollTarget.on('scroll.scrolling_element', function () { - self.$el.trigger('scroll'); - }); + delete this.dragState.currentDropzoneEl; + } }, /** * Called when the snippet is dropped after being dragged thanks to the @@ -1332,7 +1329,7 @@ var SnippetEditor = Widget.extend({ * @param {Event} ev * @param {Object} ui */ - _onDragAndDropStop: function (ev, ui) { + _onDragAndDropStop({ x, y }) { this.options.wysiwyg.odooEditor.automaticStepActive(); this.options.wysiwyg.odooEditor.automaticStepSkipStack(); this.options.wysiwyg.odooEditor.unbreakableStepUnactive(); @@ -1341,8 +1338,8 @@ var SnippetEditor = Widget.extend({ if (rowEl && rowEl.classList.contains('o_grid_mode')) { // Case when dropping the column in a grid. - // Removing the event listener. - this.$body[0].removeEventListener('mousemove', this.onDragMove, false); + // Disable dragMove handler + this.dragState.gridMode = false; // Defining the column grid area with its position. const gridProp = gridUtils._getGridProperties(rowEl); @@ -1374,7 +1371,7 @@ var SnippetEditor = Widget.extend({ // TODO lot of this is duplicated code of the d&d feature of snippets if (!this.dropped) { - let $el = $(closest(this.$body[0].querySelectorAll('.oe_drop_zone'), {x: ui.position.left, y: ui.position.top})); + let $el = $(closest(this.$body[0].querySelectorAll('.oe_drop_zone'), {x, y})); // Some drop zones might have been disabled. $el = $el.filter(this.$dropZones); if ($el.length) { @@ -1429,7 +1426,6 @@ var SnippetEditor = Widget.extend({ this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropMoveSnippet'); } - this.$dropZones.droppable('destroy'); this.$editable.find('.oe_drop_zone').remove(); var prev = this.$target.first()[0].previousSibling; @@ -1470,10 +1466,6 @@ var SnippetEditor = Widget.extend({ this.trigger_up('drag_and_drop_stop', { $snippet: this.$target, }); - this.draggableComponent.$scrollTarget.off('scroll.scrolling_element'); - if (this.draggableComponentImgs) { - this.draggableComponentImgs.$scrollTarget.off('scroll.scrolling_element'); - } const samePositionAsStart = this.$target[0].classList.contains('o_grid_item') ? (this.$target[0].parentNode === this.dragState.startingGrid && this.$target[0].style.gridArea === this.dragState.prevGridArea) @@ -1650,7 +1642,10 @@ var SnippetEditor = Widget.extend({ * @private * @param {Event} ev */ - _onDragMove(ev) { + _onDragMove({ x, y }) { + if (!this.dragState.gridMode || !this.dragState.currentDropzoneEl) { + return; + } const columnEl = this.$target[0]; const rowEl = columnEl.parentNode; @@ -1664,9 +1659,9 @@ var SnippetEditor = Widget.extend({ const columnWidth = columnEl.clientWidth + 2 * borderWidth; // Placing the column where the mouse is. - let top = ev.pageY - rowElTop - this.mousePositionYOnElement; + let top = y - rowElTop - this.mousePositionYOnElement; const bottom = top + columnHeight; - let left = ev.pageX - rowElLeft - this.mousePositionXOnElement; + let left = x - rowElLeft - this.mousePositionXOnElement; // Horizontal & vertical overflow. left = clamp(left, 0, rowEl.clientWidth - columnWidth); @@ -1912,7 +1907,7 @@ var SnippetsMenu = Widget.extend({ id: 'oe_manipulators', }); this.$body.prepend(this.$snippetEditorArea); - this.options.getScrollOptions = this._getScrollOptions.bind(this); + this.options.getDragAndDropOptions = this._getDragAndDropOptions.bind(this); // Add tooltips on we-title elements whose text overflows and on all // elements with available tooltip text. Note that the tooltips of the @@ -3068,7 +3063,7 @@ var SnippetsMenu = Widget.extend({ this.$el.html($html); this.$el.append(this.customizePanel); this.$el.append(this.invisibleDOMPanelEl); - this._makeSnippetDraggable(this.$snippets); + this._makeSnippetDraggable(); this._disableUndroppableSnippets(); this.$el.addClass('o_loaded'); @@ -3212,18 +3207,14 @@ var SnippetsMenu = Widget.extend({ * @param {Object} [options={}] * @returns {Object} */ - _getScrollOptions(options = {}) { + _getDragAndDropOptions(options = {}) { + let iframeWindow = false; + if (this.$body[0].ownerDocument.defaultView !== window) { + iframeWindow = this.$body[0].ownerDocument.defaultView; + } return Object.assign({}, options, { - scrollBoundaries: Object.assign({ - right: false, - }, options.scrollBoundaries), - jQueryDraggableOptions: Object.assign({ - appendTo: this.$body, - cursor: 'move', - greedy: true, - scroll: false, - }, options.jQueryDraggableOptions), - disableHorizontalScroll: true, + iframeWindow, + cursor: "move", }); }, /** @@ -3285,10 +3276,11 @@ var SnippetsMenu = Widget.extend({ * Make given snippets be draggable/droppable thanks to their thumbnail. * * @private - * @param {jQuery} $snippets */ - _makeSnippetDraggable: function ($snippets) { - var self = this; + _makeSnippetDraggable() { + if (this.draggableComponent) { + this.draggableComponent.destroy(); + } var $toInsert, dropped, $snippet; let $dropZones; @@ -3298,178 +3290,179 @@ var SnippetsMenu = Widget.extend({ $scrollingElement = $(this.ownerDocument).find('.o_editable'); } - const smoothScrollOptions = this._getScrollOptions({ - jQueryDraggableOptions: { - handle: '.oe_snippet_thumbnail:not(.o_we_already_dragging)', - cancel: '.oe_snippet.o_disabled', - helper: function () { - const dragSnip = this.cloneNode(true); - dragSnip.querySelectorAll('.o_delete_btn, .o_rename_btn').forEach( - el => el.remove() - ); - self.$el[0].ownerDocument.body.append(dragSnip); - return dragSnip; - }, - start: function () { - self._hideSnippetTooltips(); + const dragAndDropOptions = this.options.getDragAndDropOptions({ + el: this.$el[0], + elements: ".oe_snippet", + scrollingElement: $scrollingElement[0], + handle: '.oe_snippet_thumbnail:not(.o_we_already_dragging)', + cancel: '.oe_snippet.o_disabled', + dropzones: () => { + return $dropZones.toArray(); + }, + helper: ({ element, elementRect, helperOffset, x, y }) => { + const dragSnip = element.cloneNode(true); + dragSnip.querySelectorAll('.o_delete_btn, .o_rename_btn').forEach( + el => el.remove() + ); + dragSnip.style.position = "fixed"; + this.$el[0].ownerDocument.body.append(dragSnip); + // Prepare the offset of the helper to be at the position it was dragged from + helperOffset.x = x - elementRect.x; + helperOffset.y = y - elementRect.y; + return dragSnip; + }, + onDragStart: ({ element }) => { + this._hideSnippetTooltips(); - const prom = new Promise(resolve => dragAndDropResolve = () => resolve()); - self._mutex.exec(() => prom); + const prom = new Promise(resolve => dragAndDropResolve = () => resolve()); + this._mutex.exec(() => prom); - const doc = self.options.wysiwyg.odooEditor.document; - $(doc.body).addClass('oe_dropzone_active'); + const doc = this.options.wysiwyg.odooEditor.document; + $(doc.body).addClass('oe_dropzone_active'); - self.options.wysiwyg.odooEditor.automaticStepUnactive(); + this.options.wysiwyg.odooEditor.automaticStepUnactive(); - self.$el.find('.oe_snippet_thumbnail').addClass('o_we_already_dragging'); - self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet'); + this.$el.find('.oe_snippet_thumbnail').addClass('o_we_already_dragging'); + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet'); - dropped = false; - $snippet = $(this); - var $baseBody = $snippet.find('.oe_snippet_body'); - var $selectorSiblings = $(); - var $selectorChildren = $(); - const selectorExcludeAncestor = []; - var temp = self.templateOptions; - for (var k in temp) { - if ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) { - if (temp[k]['drop-near']) { - $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all()); - } - if (temp[k]['drop-in']) { - $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all()); - } - if (temp[k]['drop-exclude-ancestor']) { - selectorExcludeAncestor.push(temp[k]['drop-exclude-ancestor']); - } + dropped = false; + $snippet = $(element); + var $baseBody = $snippet.find('.oe_snippet_body'); + var $selectorSiblings = $(); + var $selectorChildren = $(); + const selectorExcludeAncestor = []; + var temp = this.templateOptions; + for (var k in temp) { + if ($baseBody.is(temp[k].base_selector) && !$baseBody.is(temp[k].base_exclude)) { + if (temp[k]['drop-near']) { + $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all()); + } + if (temp[k]['drop-in']) { + $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all()); + } + if (temp[k]['drop-exclude-ancestor']) { + selectorExcludeAncestor.push(temp[k]['drop-exclude-ancestor']); } } + } - // Prevent dropping an element into another one. - // (E.g. ToC inside another ToC) - for (const excludedAncestorSelector of selectorExcludeAncestor) { - $selectorSiblings = $selectorSiblings.filter((i, el) => !el.closest(excludedAncestorSelector)); - $selectorChildren = $selectorChildren.filter((i, el) => !el.closest(excludedAncestorSelector)); - } + // Prevent dropping an element into another one. + // (E.g. ToC inside another ToC) + for (const excludedAncestorSelector of selectorExcludeAncestor) { + $selectorSiblings = $selectorSiblings.filter((i, el) => !el.closest(excludedAncestorSelector)); + $selectorChildren = $selectorChildren.filter((i, el) => !el.closest(excludedAncestorSelector)); + } - $toInsert = $baseBody.clone(); - // Color-customize dynamic SVGs in dropped snippets with current theme colors. - [...$toInsert.find('img[src^="/web_editor/shape/"]')].forEach(dynamicSvg => { - const colorCustomizedURL = new URL(dynamicSvg.getAttribute('src'), window.location.origin); - colorCustomizedURL.searchParams.forEach((value, key) => { - const match = key.match(/^c([1-5])$/); - if (match) { - colorCustomizedURL.searchParams.set(key, weUtils.getCSSVariableValue(`o-color-${match[1]}`)); - } - }); - dynamicSvg.src = colorCustomizedURL.pathname + colorCustomizedURL.search; + $toInsert = $baseBody.clone(); + // Color-customize dynamic SVGs in dropped snippets with current theme colors. + [...$toInsert.find('img[src^="/web_editor/shape/"]')].forEach(dynamicSvg => { + const colorCustomizedURL = new URL(dynamicSvg.getAttribute('src'), window.location.origin); + colorCustomizedURL.searchParams.forEach((value, key) => { + const match = key.match(/^c([1-5])$/); + if (match) { + colorCustomizedURL.searchParams.set(key, weUtils.getCSSVariableValue(`o-color-${match[1]}`)); + } }); + dynamicSvg.src = colorCustomizedURL.pathname + colorCustomizedURL.search; + }); - if (!$selectorSiblings.length && !$selectorChildren.length) { - console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + " have not insert action: data-drop-near or data-drop-in"); - return; - } + if (!$selectorSiblings.length && !$selectorChildren.length) { + console.warn($snippet.find('.oe_snippet_thumbnail_title').text() + " have not insert action: data-drop-near or data-drop-in"); + return; + } - const forbidSanitize = $snippet.data('oeForbidSanitize'); - const canBeSanitizedUnless = forbidSanitize === 'form' ? 'form' : !forbidSanitize; - // Specific case for inline snippet (e.g. "s_badge") + const forbidSanitize = $snippet.data('oeForbidSanitize'); + const canBeSanitizedUnless = forbidSanitize === 'form' ? 'form' : !forbidSanitize; + // Specific case for inline snippet (e.g. "s_badge") $baseBody[0].classList.remove("oe_snippet_body"); const toInsertInline = window.getComputedStyle($baseBody[0]).display.includes('inline'); $baseBody[0].classList.add("oe_snippet_body"); - self._activateInsertionZones($selectorSiblings, $selectorChildren, canBeSanitizedUnless, toInsertInline); - $dropZones = self.getEditableArea().find('.oe_drop_zone'); - if (forbidSanitize === 'form') { - $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize]:not([data-oe-sanitize="allow_form"]) .oe_drop_zone')); - } else if (forbidSanitize) { - $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize] .oe_drop_zone')); + this._activateInsertionZones($selectorSiblings, $selectorChildren, canBeSanitizedUnless, toInsertInline); + $dropZones = this.getEditableArea().find('.oe_drop_zone'); + if (forbidSanitize === 'form') { + $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize]:not([data-oe-sanitize="allow_form"]) .oe_drop_zone')); + } else if (forbidSanitize) { + $dropZones = $dropZones.filter((i, el) => !el.closest('[data-oe-sanitize] .oe_drop_zone')); + } + // If a modal is open, the scroll target must be that modal + const $openModal = this.getEditableArea().find('.modal:visible'); + if ($openModal.length) { + this.draggableComponent.update({ scrollingElement: $openModal[0]}); + $scrollingElement = $openModal; + } + this.trigger_up('drop_zone_start'); + }, + dropzoneOver: ({ dropzone }) => { + if (dropped) { + $toInsert.detach(); + $toInsert.addClass('oe_snippet_body'); + [...$dropZones].forEach(dropzoneEl => + dropzoneEl.classList.remove("invisible")); + } + dropped = true; + $(dropzone.el).first().after($toInsert).addClass('invisible'); + $toInsert.removeClass('oe_snippet_body'); + this.trigger_up('drop_zone_over'); + }, + dropzoneOut: ({ dropzone }) => { + var prev = $toInsert.prev(); + if (dropzone.el === prev[0]) { + dropped = false; + $toInsert.detach(); + $(dropzone.el).removeClass('invisible'); + $toInsert.addClass('oe_snippet_body'); + } + this.trigger_up('drop_zone_out'); + }, + onDragEnd: async ({ x, y, helper }) => { + const doc = this.options.wysiwyg.odooEditor.document; + $(doc.body).removeClass('oe_dropzone_active'); + this.options.wysiwyg.odooEditor.automaticStepUnactive(); + this.options.wysiwyg.odooEditor.automaticStepSkipStack(); + $toInsert.removeClass('oe_snippet_body'); + $scrollingElement.off('scroll.scrolling_element'); + if (!dropped && y > 3 && x + helper.getBoundingClientRect().height < this.el.getBoundingClientRect().left) { + const point = { x, y }; + let droppedOnNotNearest = touching(doc.body.querySelectorAll('.oe_structure_not_nearest'), point); + // If dropped outside of a dropzone with class oe_structure_not_nearest, + // move the snippet to the nearest dropzone without it + const selector = droppedOnNotNearest + ? '.oe_drop_zone' + : ':not(.oe_structure_not_nearest) > .oe_drop_zone'; + let $el = $(closest(doc.body.querySelectorAll(selector), point)); + // Some drop zones might have been disabled. + $el = $el.filter($dropZones); + if ($el.length) { + $el.after($toInsert); + dropped = true; } - $dropZones.droppable({ - over: function () { - if (dropped) { - $toInsert.detach(); - $toInsert.addClass('oe_snippet_body'); - [...$dropZones].forEach(dropzoneEl => - dropzoneEl.classList.remove("invisible")); - } - dropped = true; - $(this).first().after($toInsert).addClass('invisible'); - $toInsert.removeClass('oe_snippet_body'); - self.trigger_up('drop_zone_over'); - }, - out: function () { - var prev = $toInsert.prev(); - if (this === prev[0]) { - dropped = false; - $toInsert.detach(); - $(this).removeClass('invisible'); - $toInsert.addClass('oe_snippet_body'); - } - self.trigger_up('drop_zone_out'); - }, - }); - // If a modal is open, the scroll target must be that modal - const $openModal = self.getEditableArea().find('.modal:visible'); - if ($openModal.length) { - self.draggableComponent.$scrollTarget = $openModal; + } + + this.getEditableArea().find('.oe_drop_zone').remove(); + + let $toInsertParent; + let prev; + let next; + if (dropped) { + prev = $toInsert.first()[0].previousSibling; + next = $toInsert.last()[0].nextSibling; + + $toInsertParent = $toInsert.parent(); + $toInsert.detach(); + } + + this.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet'); + + if (dropped) { + if (prev) { + $toInsert.insertAfter(prev); + } else if (next) { + $toInsert.insertBefore(next); + } else { + $toInsertParent.prepend($toInsert); } - // Trigger a scroll on the draggable element so that jQuery updates - // the position of the drop zones. - self.draggableComponent.$scrollTarget.on('scroll.scrolling_element', function () { - self.$el.trigger('scroll'); - }); - self.trigger_up('drop_zone_start'); - }, - stop: async function (ev, ui) { - const doc = self.options.wysiwyg.odooEditor.document; - $(doc.body).removeClass('oe_dropzone_active'); - self.options.wysiwyg.odooEditor.automaticStepUnactive(); - self.options.wysiwyg.odooEditor.automaticStepSkipStack(); - $toInsert.removeClass('oe_snippet_body'); - self.draggableComponent.$scrollTarget.off('scroll.scrolling_element'); - if (!dropped && ui.position.top > 3 && ui.position.left + ui.helper.outerHeight() < self.el.getBoundingClientRect().left) { - const point = {x: ui.position.left, y: ui.position.top}; - let droppedOnNotNearest = touching(doc.body.querySelectorAll('.oe_structure_not_nearest'), point); - // If dropped outside of a dropzone with class oe_structure_not_nearest, - // move the snippet to the nearest dropzone without it - const selector = droppedOnNotNearest - ? '.oe_drop_zone' - : ':not(.oe_structure_not_nearest) > .oe_drop_zone'; - let $el = $(closest(doc.body.querySelectorAll(selector), point)); - // Some drop zones might have been disabled. - $el = $el.filter($dropZones); - if ($el.length) { - $el.after($toInsert); - dropped = true; - } - } - - $dropZones.droppable('destroy'); - self.getEditableArea().find('.oe_drop_zone').remove(); - - let $toInsertParent; - let prev; - let next; - if (dropped) { - prev = $toInsert.first()[0].previousSibling; - next = $toInsert.last()[0].nextSibling; - - $toInsertParent = $toInsert.parent(); - $toInsert.detach(); - } - - self.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet'); - - if (dropped) { - if (prev) { - $toInsert.insertAfter(prev); - } else if (next) { - $toInsert.insertBefore(next); - } else { - $toInsertParent.prepend($toInsert); - } - - var $target = $toInsert; + var $target = $toInsert; if ($target[0].classList.contains("o_snippet_drop_in_only")) { // If it's a "drop in only" snippet, after dropping @@ -3481,36 +3474,35 @@ var SnippetsMenu = Widget.extend({ delete $target[0].dataset.name; } - self.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet'); - await self._scrollToSnippet($target, self.$scrollable); - self.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet'); + this.options.wysiwyg.odooEditor.observerUnactive('dragAndDropCreateSnippet'); + await this._scrollToSnippet($target, this.$scrollable); + this.options.wysiwyg.odooEditor.observerActive('dragAndDropCreateSnippet'); - browser.setTimeout(async () => { - // Free the mutex now to allow following operations - // (mutexed as well). - dragAndDropResolve(); + browser.setTimeout(async () => { + // Free the mutex now to allow following operations + // (mutexed as well). + dragAndDropResolve(); - await self.callPostSnippetDrop($target); + await this.callPostSnippetDrop($target); - // Restore editor to its normal edition state, also - // make sure the undroppable snippets are updated. - self._disableUndroppableSnippets(); - self.options.wysiwyg.odooEditor.unbreakableStepUnactive(); - self.options.wysiwyg.odooEditor.historyStep(); - self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging'); - }); - } else { - $toInsert.remove(); - if (dragAndDropResolve) { - dragAndDropResolve(); - } - self.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging'); + // Restore editor to its normal edition state, also + // make sure the undroppable snippets are updated. + this._disableUndroppableSnippets(); + this.options.wysiwyg.odooEditor.unbreakableStepUnactive(); + this.options.wysiwyg.odooEditor.historyStep(); + this.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging'); + }); + } else { + $toInsert.remove(); + if (dragAndDropResolve) { + dragAndDropResolve(); } - self.trigger_up('drop_zone_stop'); - }, + this.$el.find('.oe_snippet_thumbnail').removeClass('o_we_already_dragging'); + } + this.trigger_up('drop_zone_stop'); }, }); - this.draggableComponent = new SmoothScrollOnDrag($snippets, $scrollingElement, smoothScrollOptions); + this.draggableComponent = useDragAndDrop({ ref: { el: this.el }, ...dragAndDropOptions }); }, /** * Adds the 'o_default_snippet_text' class on nodes which contain only diff --git a/addons/web_editor/static/src/js/wysiwyg/MoveNodePlugin.js b/addons/web_editor/static/src/js/wysiwyg/MoveNodePlugin.js index edb3b7fd35b..12fde5fa913 100644 --- a/addons/web_editor/static/src/js/wysiwyg/MoveNodePlugin.js +++ b/addons/web_editor/static/src/js/wysiwyg/MoveNodePlugin.js @@ -1,12 +1,39 @@ /** @odoo-module */ - import { ancestors, closestElement, resetOuids, setSelection, } from '@web_editor/js/editor/odoo-editor/src/OdooEditor'; -import { SmoothScrollOnDrag } from "@web_editor/js/editor/smooth_scroll_on_drag"; +import { useNativeDraggable } from "@web_editor/js/editor/drag_and_drop"; + +const simpleDraggableHook = { + acceptedParams: { + helper: [Function], + }, + edgeScrolling: { enable: true }, + onComputeParams({ ctx, params }) { + ctx.helper = params.helper; + ctx.followCursor = false; + }, + onDragStart({ ctx }) { + ctx.current.element = ctx.helper(); + ctx.current.element.style.left = `${ctx.pointer.x + 10}px`; + ctx.current.element.style.top = `${ctx.pointer.y + 10}px`; + ctx.current.element.style.position = "fixed"; + // makeDraggableHook disables pointer events, we want them in this case + document.body.classList.remove("pe-none"); + return ctx.current; + }, + onDrag({ ctx }) { + ctx.current.element.style.left = `${ctx.pointer.x}px`; + ctx.current.element.style.top = `${ctx.pointer.y}px`; + }, + onDragEnd({ ctx }) { + ctx.current.element.remove(); + return ctx.current; + }, +}; const WIDGET_CONTAINER_WIDTH = 25; const WIDGET_MOVE_SIZE = 20; @@ -231,28 +258,24 @@ export class MoveNodePlugin { if (this._scrollableElement) { this.smoothScrollOnDrag && this.smoothScrollOnDrag.destroy(); - this.smoothScrollOnDrag = new SmoothScrollOnDrag($(this._moveWidget), $(this._scrollableElement), { - scrollBoundaries: { - right: false, - }, - jQueryDraggableOptions: { - start: () => this._startDropzones(movableElement, containerRect), - stop: () => this._stopDropzones(movableElement), - helper: () => { - const container = document.createElement('div'); - container.append(movableElement.cloneNode(true)) - const style = getComputedStyle(movableElement); - container.style.height = style.height; - container.style.width = style.width; - container.style.paddingLeft = '25px'; - container.style.opacity = '0.4'; - return $(container); - }, - appendTo: this._dragHelperContainer, - cursor: 'move', - scroll: false, - }, - disableHorizontalScroll: true, + // TODO: This should be made more generic, one hook for the entire + // editable with each element handled. + this.smoothScrollOnDrag = useNativeDraggable(simpleDraggableHook, { + ref: { el: this._widgetContainer }, + elements: ".oe-sidewidget-move", + onDragStart: () => this._startDropzones(movableElement, containerRect), + onDragEnd: () => this._stopDropzones(movableElement), + helper: () => { + const container = document.createElement('div'); + container.append(movableElement.cloneNode(true)); + const style = getComputedStyle(movableElement); + container.style.height = style.height; + container.style.width = style.width; + container.style.paddingLeft = '25px'; + container.style.opacity = '0.4'; + this._dragHelperContainer.append(container); + return container; + } }); } } diff --git a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss index c36f44e4fc3..c2e194e34d9 100644 --- a/addons/web_editor/static/src/scss/wysiwyg_snippets.scss +++ b/addons/web_editor/static/src/scss/wysiwyg_snippets.scss @@ -25,7 +25,7 @@ width: 77px; background-color: $o-we-sidebar-blocks-content-snippet-bg; - &.ui-draggable-dragging { + &.o_draggable_dragging { transform: rotate(-3deg) scale(1.2); box-shadow: 0 5px 25px -10px black; transition: transform 0.3s, box-shadow 0.3s; @@ -2467,7 +2467,7 @@ we-select.o_we_border_preview_aligned_select { pointer-events: auto; } } -.oe_overlay.ui-draggable-dragging .o_handles { +.oe_overlay.o_draggable_dragging .o_handles { display: none; } diff --git a/addons/website/static/src/js/editor/snippets.editor.js b/addons/website/static/src/js/editor/snippets.editor.js index f6a4d095399..b2b1a2c8a98 100644 --- a/addons/website/static/src/js/editor/snippets.editor.js +++ b/addons/website/static/src/js/editor/snippets.editor.js @@ -203,7 +203,10 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ /** * @override */ - _getScrollOptions(options = {}) { + _getDragAndDropOptions(options = {}) { + // TODO: This is currently not in use by Odoo's D&D + // There is currently no way in Odoo D&D to offset the edge scrolling. + // When there is, this code should be adapted. const finalOptions = this._super(...arguments); if (!options.offsetElements || !options.offsetElements.$top) { const $header = $('#top'); @@ -212,7 +215,6 @@ const wSnippetMenu = weSnippetEditor.SnippetsMenu.extend({ finalOptions.offsetElements.$top = $header; } } - finalOptions.jQueryDraggableOptions.iframeFix = true; return finalOptions; }, /** diff --git a/addons/website/static/src/js/tours/tour_utils.js b/addons/website/static/src/js/tours/tour_utils.js index 919f405cec3..1a12b1129da 100644 --- a/addons/website/static/src/js/tours/tour_utils.js +++ b/addons/website/static/src/js/tours/tour_utils.js @@ -69,7 +69,7 @@ function selectColorPalette(position = "left") { function changeColumnSize(position = "right") { return { - trigger: `iframe .oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.e`, + trigger: `iframe .oe_overlay.o_draggable.o_we_overlay_sticky.oe_active .o_handle.e`, content: markup(_t("Slide this button to change the column size.")), position: position, }; @@ -133,7 +133,7 @@ function changePaddingSize(direction) { position = "bottom"; } return { - trigger: `iframe .oe_overlay.ui-draggable.o_we_overlay_sticky.oe_active .o_handle.${paddingDirection}`, + trigger: `iframe .oe_overlay.o_draggable.o_we_overlay_sticky.oe_active .o_handle.${paddingDirection}`, content: markup(_t("Slide this button to change the %s padding", direction)), consumeEvent: 'mousedown', position: position, @@ -239,7 +239,7 @@ function dragNDrop(snippet, position = "bottom") { position: position, // Normally no main snippet can be dropped in the default footer but // targeting it allows to force "dropping at the end of the page". - run: "drag_and_drop iframe #wrapwrap > footer", + run: "drag_and_drop_native iframe #wrapwrap > footer", }; } diff --git a/addons/website/static/tests/tours/conditional_visibility.js b/addons/website/static/tests/tours/conditional_visibility.js index 0515bbdb801..c4c884ef372 100644 --- a/addons/website/static/tests/tours/conditional_visibility.js +++ b/addons/website/static/tests/tours/conditional_visibility.js @@ -171,8 +171,8 @@ wTourUtils.clickOnSnippet(snippets[0]), wTourUtils.clickOnSnippet(snippets[1]), { content: "Drag the 'Banner' snippet to the end of the page", - trigger: "iframe .o_overlay_move_options .ui-draggable-handle", - run: "drag_and_drop iframe #wrapwrap footer", + trigger: "iframe .o_overlay_move_options .o_move_handle", + run: "drag_and_drop_native iframe #wrapwrap footer", }, ...checkEyesIconAfterSave(false), { diff --git a/addons/website/static/tests/tours/focus_blur_snippets.js b/addons/website/static/tests/tours/focus_blur_snippets.js index 363c7c4d249..3723e348bca 100644 --- a/addons/website/static/tests/tours/focus_blur_snippets.js +++ b/addons/website/static/tests/tours/focus_blur_snippets.js @@ -29,7 +29,7 @@ function clickAndCheck(blockID, expected) { }, { content: 'Once the related overlays are enabled/disabled, check that the focus/blur calls have been correct.', trigger: blockID - ? `iframe .oe_overlay.ui-draggable:eq(${blockData.overlayIndex}).oe_active` + ? `iframe .oe_overlay.o_draggable:eq(${blockData.overlayIndex}).oe_active` : `iframe #oe_manipulators:not(:has(.oe_active))`, allowInvisible: !blockID, run: function (actions) { @@ -57,7 +57,7 @@ wTourUtils.registerWebsitePreviewTour("focus_blur_snippets", { { content: 'Drag the custom block into the page', trigger: '#snippet_structure .oe_snippet:has(.oe_snippet_body.s_focusblur) .oe_snippet_thumbnail', - run: 'drag_and_drop iframe #wrap', + run: 'drag_and_drop_native iframe #wrap', }, ...clickAndCheck('parent', ['focus parent']), ...clickAndCheck(null, ['blur parent']), diff --git a/addons/website/static/tests/tours/html_editor.js b/addons/website/static/tests/tours/html_editor.js index d5296d9b5a7..a931793175a 100644 --- a/addons/website/static/tests/tours/html_editor.js +++ b/addons/website/static/tests/tours/html_editor.js @@ -16,7 +16,7 @@ wTourUtils.registerWebsitePreviewTour('html_editor_multiple_templates', { content: "drop a snippet", trigger: ".oe_snippet:has(.s_cover) .oe_snippet_thumbnail", // id starting by 'oe_structure..' will actually create an inherited view - run: "drag_and_drop iframe #oe_structure_test_ui", + run: "drag_and_drop_native iframe #oe_structure_test_ui", }, ...wTourUtils.clickOnSave(), // 2. Edit generic view diff --git a/addons/website/static/tests/tours/snippet_editor_panel_options.js b/addons/website/static/tests/tours/snippet_editor_panel_options.js index 155a4c81ed2..5e947a5c4f1 100644 --- a/addons/website/static/tests/tours/snippet_editor_panel_options.js +++ b/addons/website/static/tests/tours/snippet_editor_panel_options.js @@ -107,7 +107,7 @@ wTourUtils.dragNDrop({ trigger: '.snippet-option-layout_column [data-select-count="3"]', }, { content: "The snippet should have the correct number of columns.", - trigger: 'iframe .s_text_block .container > .row', + trigger: 'iframe .s_text_block .container > .row .col-lg-4:eq(3)', run() { if (this.$anchor[0].childElementCount !== 3) { console.error("The snippet does not have the correct number of columns"); diff --git a/addons/website/static/tests/tours/snippet_popup_and_animations.js b/addons/website/static/tests/tours/snippet_popup_and_animations.js index 477bb6eb87b..50989d3dc2c 100644 --- a/addons/website/static/tests/tours/snippet_popup_and_animations.js +++ b/addons/website/static/tests/tours/snippet_popup_and_animations.js @@ -83,7 +83,7 @@ wTourUtils.registerWebsitePreviewTour("snippet_popup_and_animations", { content: "Drag the Columns building block and drop it at the bottom of the popup.", trigger: '#oe_snippets .oe_snippet[name="Columns"] .oe_snippet_thumbnail:not(.o_we_already_dragging)', extra_trigger: ".o_website_preview.editor_enable.editor_has_snippets", - run: "drag_and_drop iframe #wrapwrap .modal-content .s_media_list .container > .row > :last-child", + run: "drag_and_drop_native iframe #wrapwrap .modal-content .s_media_list .container > .row > :last-child", }, wTourUtils.clickOnElement("3rd columns", "iframe .s_popup .s_three_columns .row > :last-child"), ...setOnScrollAnim(), diff --git a/addons/website/static/tests/tours/snippet_popup_and_scrollbar.js b/addons/website/static/tests/tours/snippet_popup_and_scrollbar.js index 89419da10b7..e55f22ce333 100644 --- a/addons/website/static/tests/tours/snippet_popup_and_scrollbar.js +++ b/addons/website/static/tests/tours/snippet_popup_and_scrollbar.js @@ -52,7 +52,7 @@ wTourUtils.registerWebsitePreviewTour("snippet_popup_and_scrollbar", { { content: "Drag the Media List block and drop it in the popup.", trigger: "#oe_snippets .oe_snippet:has(> [data-snippet='s_media_list']) .oe_snippet_thumbnail", - run: "drag_and_drop iframe #wrap .s_popup .modal-content.oe_structure", + run: "drag_and_drop_native iframe #wrap .s_popup .modal-content.oe_structure", }, checkScrollbar(false), { @@ -89,7 +89,7 @@ wTourUtils.registerWebsitePreviewTour("snippet_popup_and_scrollbar", { { content: "Drag the Media List block and drop it in the popup.", trigger: "#oe_snippets .oe_snippet:has(> [data-snippet='s_media_list']) .oe_snippet_thumbnail", - run: "drag_and_drop iframe #wrap .s_popup .modal-content.oe_structure", + run: "drag_and_drop_native iframe #wrap .s_popup .modal-content.oe_structure", }, checkScrollbar(false), { @@ -108,7 +108,7 @@ wTourUtils.registerWebsitePreviewTour("snippet_popup_and_scrollbar", { { content: "Drag a Media List snippet and drop it in the Cookies Bar.", trigger: "#oe_snippets .oe_snippet:has(> [data-snippet='s_media_list']) .oe_snippet_thumbnail", - run: "drag_and_drop iframe #website_cookies_bar .modal-content.oe_structure", + run: "drag_and_drop_native iframe #website_cookies_bar .modal-content.oe_structure", }, { content: "Select the Media List snippet in the Cookies Bar.", diff --git a/addons/website/static/tests/tours/snippet_table_of_content.js b/addons/website/static/tests/tours/snippet_table_of_content.js index 2f82ebd30a7..ddbfa45e717 100644 --- a/addons/website/static/tests/tours/snippet_table_of_content.js +++ b/addons/website/static/tests/tours/snippet_table_of_content.js @@ -47,6 +47,8 @@ wTourUtils.registerWebsitePreviewTour('snippet_table_of_content', { content: "Hide the first TOC on mobile", trigger: '[data-toggle-device-visibility="no_mobile"]', }, + // Go back to blocks tabs to avoid changing the first ToC options + wTourUtils.goBackToBlocks(), { content: "Click on the second TOC's title", trigger: 'iframe .s_table_of_content:eq(1) h2', diff --git a/addons/website/static/tests/tours/snippet_version.js b/addons/website/static/tests/tours/snippet_version.js index e0d0a69e1c0..94a1ffc7427 100644 --- a/addons/website/static/tests/tours/snippet_version.js +++ b/addons/website/static/tests/tours/snippet_version.js @@ -17,10 +17,10 @@ wTourUtils.registerWebsitePreviewTour("snippet_version", { }), { content: "Test t-snippet and t-snippet-call: snippets have data-snippet set", - trigger: '#oe_snippets .o_panel_body > .oe_snippet.ui-draggable', + trigger: '#oe_snippets .o_panel_body > .oe_snippet', run: function () { // Tests done here as all these are not visible on the page - const draggableSnippets = document.querySelectorAll('#oe_snippets .o_panel_body > .oe_snippet.ui-draggable > :nth-child(2)'); + const draggableSnippets = document.querySelectorAll('#oe_snippets .o_panel_body > .oe_snippet.o_draggable > :nth-child(2)'); if (![...draggableSnippets].every(el => el.dataset.snippet)) { console.error("error Some t-snippet are missing their template name"); } diff --git a/addons/website/static/tests/tours/snippets_all_drag_and_drop.js b/addons/website/static/tests/tours/snippets_all_drag_and_drop.js index 3974bf9ed7e..48092afd0e3 100644 --- a/addons/website/static/tests/tours/snippets_all_drag_and_drop.js +++ b/addons/website/static/tests/tours/snippets_all_drag_and_drop.js @@ -41,7 +41,7 @@ for (const snippet of snippetsNames) { const snippetSteps = [{ content: `Drop ${snippet} snippet [${n}/${snippetsNames.length}]`, trigger: `#oe_snippets .oe_snippet:has( > [data-snippet='${snippet}']) .oe_snippet_thumbnail`, - run: "drag_and_drop iframe #wrap", + run: "drag_and_drop_native iframe #wrap", }, { content: `Edit ${snippet} snippet`, trigger: `iframe #wrap.o_editable [data-snippet='${snippet}']${isModal ? ' .modal.show' : ''}`, diff --git a/addons/website/static/tests/tours/start_cloned_snippet.js b/addons/website/static/tests/tours/start_cloned_snippet.js index 37d8e8698a1..ace86f121a4 100644 --- a/addons/website/static/tests/tours/start_cloned_snippet.js +++ b/addons/website/static/tests/tours/start_cloned_snippet.js @@ -12,7 +12,7 @@ wTourUtils.registerWebsitePreviewTour('website_start_cloned_snippet', { id: 's_countdown', }; const dragNDropOutOfFooter = wTourUtils.dragNDrop(countdownSnippet); - dragNDropOutOfFooter.run = 'drag_and_drop iframe #wrapwrap #wrap'; + dragNDropOutOfFooter.run = 'drag_and_drop_native iframe #wrapwrap #wrap'; return [ dragNDropOutOfFooter, wTourUtils.clickOnSnippet(countdownSnippet), diff --git a/addons/website/static/tests/tours/website_form_editor.js b/addons/website/static/tests/tours/website_form_editor.js index e49fe06f44a..b3ce4af913b 100644 --- a/addons/website/static/tests/tours/website_form_editor.js +++ b/addons/website/static/tests/tours/website_form_editor.js @@ -137,7 +137,7 @@ { content: "Drop the form snippet", trigger: '#oe_snippets .oe_snippet:has(.s_website_form) .oe_snippet_thumbnail', - run: 'drag_and_drop iframe #wrap', + run: 'drag_and_drop_native iframe #wrap', }, { content: "Select form by clicking on an input field", extra_trigger: 'iframe .s_website_form_field', @@ -551,7 +551,7 @@ { content: "Add the form snippet", trigger: '#oe_snippets .oe_snippet:has(.s_website_form) .oe_snippet_thumbnail', - run: 'drag_and_drop iframe #wrap', + run: 'drag_and_drop_native iframe #wrap', }, { content: "Select the form by clicking on an input field", extra_trigger: 'iframe .s_website_form_field', diff --git a/addons/website_event/static/src/js/tours/event_tour.js b/addons/website_event/static/src/js/tours/event_tour.js index 879c574a60d..1b3782b57c2 100644 --- a/addons/website_event/static/src/js/tours/event_tour.js +++ b/addons/website_event/static/src/js/tours/event_tour.js @@ -22,7 +22,7 @@ patch(EventAdditionalTourSteps.prototype, { trigger: '#oe_snippets.o_loaded div[name="Image - Text"] .oe_snippet_thumbnail', content: markup(_t("Drag and Drop this snippet below the event title.")), position: 'bottom', - run: 'drag_and_drop iframe #o_wevent_event_main_col', + run: 'drag_and_drop_native iframe #o_wevent_event_main_col', }, { trigger: 'button[data-action="save"]', content: markup(_t("Don't forget to click save when you're done.")), diff --git a/addons/website_event/static/tests/tours/website_event.js b/addons/website_event/static/tests/tours/website_event.js index f9cff739fb6..cad75445e99 100644 --- a/addons/website_event/static/tests/tours/website_event.js +++ b/addons/website_event/static/tests/tours/website_event.js @@ -39,7 +39,7 @@ trigger: "#oe_snippets.o_loaded #snippet_structure .oe_snippet:eq(2) .oe_snippet_thumbnail", content: _t("Drag this block and drop it in your page."), position: "bottom", - run: "drag_and_drop", + run: "drag_and_drop_native iframe #wrapwrap > main", }, { trigger: "button[data-action=save]", content: _t("Once you click on save, your event is updated."), diff --git a/addons/website_sale/static/src/js/tours/website_sale_shop.js b/addons/website_sale/static/src/js/tours/website_sale_shop.js index f9b0fbe4072..55ae54922ac 100644 --- a/addons/website_sale/static/src/js/tours/website_sale_shop.js +++ b/addons/website_sale/static/src/js/tours/website_sale_shop.js @@ -56,7 +56,7 @@ extra_trigger: "body:not(.modal-open)", content: _t("Drag this website block and drop it in your page."), position: "bottom", - run: "drag_and_drop", + run: "drag_and_drop_native iframe #wrapwrap > main", }, { trigger: "button[data-action=save]", content: markup(_t("Once you click on Save, your product is updated.")), diff --git a/addons/website_sale/static/tests/tours/website_sale_category_page_and_products_snippet.js b/addons/website_sale/static/tests/tours/website_sale_category_page_and_products_snippet.js index a12bc3adc9e..1a5b0c18985 100644 --- a/addons/website_sale/static/tests/tours/website_sale_category_page_and_products_snippet.js +++ b/addons/website_sale/static/tests/tours/website_sale_category_page_and_products_snippet.js @@ -12,7 +12,7 @@ wTourUtils.registerWebsitePreviewTour('category_page_and_products_snippet_editio }, () => [ Object.assign(wTourUtils.dragNDrop({id: 's_dynamic_snippet_products', name: 'Products'}), { content: "Drag and drop the product snippet inside the category area", - run: 'drag_and_drop iframe #category_header', + run: 'drag_and_drop_native iframe #category_header', }), { content: "Click on the product snippet to show its options",