[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) <qsm@odoo.com>
This commit is contained in:
Arthur Detroux (ard)
2023-10-19 22:01:48 +00:00
committed by qsm-odoo
parent fae131e44c
commit 7594d71ca8
31 changed files with 683 additions and 2620 deletions
@@ -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',
@@ -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',
@@ -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']",
@@ -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";
@@ -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.
+2 -6
View File
@@ -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 {
File diff suppressed because it is too large Load Diff
@@ -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;
+1 -1
View File
@@ -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',
@@ -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);
}
@@ -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 <html>). 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'));
}
}
@@ -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
@@ -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;
}
});
}
}
@@ -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;
}
@@ -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;
},
/**
@@ -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("<b>Slide</b> 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("<b>Slide</b> 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",
};
}
@@ -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),
{
@@ -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']),
@@ -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
@@ -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");
@@ -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(),
@@ -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.",
@@ -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',
@@ -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");
}
@@ -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' : ''}`,
@@ -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),
@@ -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',
@@ -22,7 +22,7 @@ patch(EventAdditionalTourSteps.prototype, {
trigger: '#oe_snippets.o_loaded div[name="Image - Text"] .oe_snippet_thumbnail',
content: markup(_t("<b>Drag and Drop</b> 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 <b>save</b> when you're done.")),
@@ -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."),
@@ -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 <b>Save</b>, your product is updated.")),
@@ -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",