[IMP] web_editor: scroll smoothly when dragging snippets

Part of https://github.com/odoo/odoo/pull/51236
task-2176020

closes odoo/odoo#51236

Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
This commit is contained in:
Laurent Stukkens (LTU)
2020-06-12 20:29:09 +00:00
committed by qsm-odoo
parent 0d06773f14
commit ab72fe6c46
@@ -2,6 +2,7 @@ odoo.define('web_editor.snippet.editor', function (require) {
'use strict';
var concurrency = require('web.concurrency');
var Class = require('web.Class');
var core = require('web.core');
var Dialog = require('web.Dialog');
var dom = require('web.dom');
@@ -18,6 +19,169 @@ var globalSelector = {
is: () => false,
};
/**
* Component that provides smooth scroll behaviour on drag.
*
* Do not forget to call unsetDraggable to ensure proper resource clearing.
* @see {@link unsetDraggable}.
*/
const SmoothOnDragComponent = Class.extend({
/**
* @constructor
* @param {jQuery} $element The element the smooth drag has to be set on
* @param {Object} jQueryDraggableOptions The configuration to be passed to
* the jQuery draggable function
*/
init($element, jQueryDraggableOptions) {
this.$element = $element;
this.$scrollTarget = $('html');
this.autoScrollHandler = null;
this.cursorAt = jQueryDraggableOptions.cursorAt || {left: 0, top: 0};
this.draggableOffset = 0;
this.mainNavBarHeight = $('.o_main_navbar').innerHeight();
this.scrollOffsetThreshold = 150;
this.scrollStep = 20;
this.scrollStepDirection = 1;
this.scrollStepDirectionEnum = {up: -1, down: 1};
this.scrollDecelerator = 0;
this.scrollTimer = 5;
this.visibleBottomOffset = 0;
this.visibleTopOffset = 0;
jQueryDraggableOptions.scroll = false;
const draggableOptions = Object.assign({}, jQueryDraggableOptions, {
start: (ev, ui) => this._onSmoothDragStart(ev, ui, jQueryDraggableOptions.start),
drag: (ev, ui) => this._onSmoothDrag(ev, ui, jQueryDraggableOptions.drag),
stop: (ev, ui) => this._onSmoothDragStop(ev, ui, jQueryDraggableOptions.stop),
});
this.$element.draggable(draggableOptions);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Ensures correct clearing of resources.
*/
unsetDraggable() {
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
*/
_startSmoothScroll() {
this._stopSmoothScroll();
this.autoScrollHandler = setInterval(
() => {
this.$scrollTarget.scrollTop(
this.$scrollTarget.scrollTop() +
this.scrollStepDirection *
this.scrollStep *
(1 - this.scrollDecelerator)
);
},
this.scrollTimer
);
},
/**
* Stops the scroll process if any is running.
*
* @private
*/
_stopSmoothScroll() {
clearInterval(this.autoScrollHandler);
},
/**
* 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 {number} dragTopOffset The offset position of the draggable helper
*/
_updatePositionOptions(dragTopOffset) {
this.draggableOffset = this.cursorAt.top + dragTopOffset;
this.visibleBottomOffset = this.$scrollTarget.scrollTop() +
this.$scrollTarget.get(0).clientHeight - this.draggableOffset;
this.visibleTopOffset = this.draggableOffset - this.$scrollTarget.scrollTop();
if (this.visibleTopOffset <= this.scrollOffsetThreshold + this.mainNavBarHeight) {
this.scrollDecelerator = this.visibleTopOffset /
(this.scrollOffsetThreshold + this.mainNavBarHeight);
this.scrollStepDirection = this.scrollStepDirectionEnum.up;
} else if (this.visibleBottomOffset <= this.scrollOffsetThreshold) {
this.scrollDecelerator = this.visibleBottomOffset /
this.scrollOffsetThreshold;
this.scrollStepDirection = this.scrollStepDirectionEnum.down;
} else {
this.scrollDecelerator = 1;
}
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* 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.offset.top);
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._updatePositionOptions(ui.offset.top);
this._startSmoothScroll();
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);
}
},
});
/**
* Management of the overlay and option list for a snippet.
*/
@@ -71,20 +235,21 @@ var SnippetEditor = Widget.extend({
// Initialize move/clone/remove buttons
if (this.isTargetMovable) {
this.dropped = false;
this.$el.draggable({
greedy: true,
this.draggableComponent = new SmoothOnDragComponent(this.$el, {
appendTo: this.$body,
cursor: 'move',
handle: '.o_move_handle',
cursorAt: {
left: 10,
top: 10
},
greedy: true,
handle: '.o_move_handle',
helper: () => {
var $clone = this.$el.clone().css({width: '24px', height: '24px', border: 0});
$clone.appendTo(this.$body).removeClass('d-none');
return $clone;
},
scroll: false,
start: this._onDragAndDropStart.bind(this),
stop: (...args) => {
// Delay our stop handler so that some summernote handlers
@@ -139,6 +304,9 @@ var SnippetEditor = Widget.extend({
*/
destroy: function () {
this._super(...arguments);
if (this.draggableComponent) {
this.draggableComponent.unsetDraggable();
}
this.$target.removeData('snippet-editor');
this.$target.off('.snippet_editor');
},
@@ -1018,6 +1186,9 @@ var SnippetsMenu = Widget.extend({
*/
destroy: function () {
this._super.apply(this, arguments);
if (this.draggableComponent) {
this.draggableComponent.unsetDraggable();
}
if (this.$window) {
this.$snippetEditorArea.remove();
this.$window.off('.snippets_menu');
@@ -1758,17 +1929,20 @@ var SnippetsMenu = Widget.extend({
var $tumb = $snippets.find('.oe_snippet_thumbnail_img:first');
var $toInsert, dropped, $snippet;
$snippets.draggable({
greedy: true,
helper: function () {
const dragSnip = this.cloneNode(true);
dragSnip.querySelectorAll('.o_delete_btn, .o_image_ribbon').forEach(el => el.remove());
return dragSnip;
},
this.draggableComponent = new SmoothOnDragComponent($snippets, {
appendTo: this.$body,
cursor: 'move',
distance: 0,
greedy: true,
handle: '.oe_snippet_thumbnail',
helper: function () {
const dragSnip = this.cloneNode(true);
dragSnip.querySelectorAll('.o_delete_btn, .o_image_ribbon').forEach(
el => el.remove()
);
return dragSnip;
},
scroll: false,
start: function () {
dropped = false;
$snippet = $(this);
@@ -2265,6 +2439,7 @@ var SnippetsMenu = Widget.extend({
return {
Class: SnippetsMenu,
Editor: SnippetEditor,
SmoothOnDragComponent: SmoothOnDragComponent,
globalSelector: globalSelector,
};
});