diff --git a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js index 6d9e519ee87..be61c08bb86 100644 --- a/addons/mail/static/tests/tours/mail_full_composer_test_tour.js +++ b/addons/mail/static/tests/tours/mail_full_composer_test_tour.js @@ -60,7 +60,7 @@ tour.register('mail/static/tests/tours/mail_full_composer_test_tour.js', { content: "Check composer content is kept", trigger: '.oe_form_field[name="body"]', run() { - const bodyContent = document.querySelector('.oe_form_field[name="body"] textarea').textContent; + const bodyContent = document.querySelector('.oe_form_field[name="body"] jw-shadow').shadowRoot.textContent; if (!bodyContent.includes("blahblah")) { console.error( `Full composer should contain text from small composer ("blahblah") in body input (actual: ${bodyContent})` diff --git a/addons/web/static/src/js/core/dom.js b/addons/web/static/src/js/core/dom.js index 4af811d41bc..ffc6638f167 100644 --- a/addons/web/static/src/js/core/dom.js +++ b/addons/web/static/src/js/core/dom.js @@ -551,9 +551,14 @@ var dom = { return; } $scrollable.stop(); - dom.scrollTo(el, Object.assign({}, options, { - duration: remainingMs, - })).then(() => resolve()); + if (document.body.contains(el)) { + dom.scrollTo(el, Object.assign({}, options, { + duration: remainingMs, + })).then(resolve); + } else { + console.warn('This snippet are wrongly removed', el); + resolve(); + } }; // Detect the end of the animation to be able to indicate it to diff --git a/addons/web/static/src/js/core/smooth_scroll_on_drag.js b/addons/web/static/src/js/core/smooth_scroll_on_drag.js index 28a4beff4c4..2ac495bd47a 100644 --- a/addons/web/static/src/js/core/smooth_scroll_on_drag.js +++ b/addons/web/static/src/js/core/smooth_scroll_on_drag.js @@ -4,48 +4,6 @@ odoo.define('web/static/src/js/core/smooth_scroll_on_drag.js', function (require const Class = require('web.Class'); const mixins = require('web.mixins'); -/** - * Provides a helper for SmoothScrollOnDrag options.offsetElements - */ -const OffsetElementsHelper = Class.extend({ - - /** - * @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 - */ - init: function (offsetElements) { - this.offsetElements = offsetElements; - }, - top: function () { - if (!this.offsetElements.$top || !this.offsetElements.$top.length) { - return 0; - } - return this.offsetElements.$top.get(0).getBoundingClientRect().bottom; - }, - right: function () { - if (!this.offsetElements.$right || !this.offsetElements.$right.length) { - return 0; - } - return this.offsetElements.$right.get(0).getBoundingClientRect().left; - }, - bottom: function () { - if (!this.offsetElements.$bottom || !this.offsetElements.$bottom.length) { - return 0; - } - return this.offsetElements.$bottom.get(0).getBoundingClientRect().top; - }, - left: function () { - if (!this.offsetElements.$left || !this.offsetElements.$left.length) { - return 0; - } - return this.offsetElements.$left.get(0).getBoundingClientRect().right; - }, -}); - /** * Provides smooth scroll behaviour on drag. */ @@ -55,7 +13,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { * @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 {jQuery} $container Container for drop element. + * scrollTarget will be found with the closest scrollable parents. * @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 @@ -76,39 +35,25 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { * 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 {Function} [options.dropzones] Function must return a JQuery list of dropzone elements. + * @param {Function} [options.over] Callback triggered when the draggable element is + * over a dropzone (requiered with options.dropzones). + * @param {Function} [options.out] Callback triggered when the draggable element is + * no longer above the dropzone (requiered with options.dropzones). */ - init(parent, $element, $scrollTarget, options = {}) { + init(parent, $element, $container, options = {}) { mixins.ParentedMixin.init.call(this); this.setParent(parent); this.$element = $element; - this.$scrollTarget = $scrollTarget; + this.$container = $container; this.options = options; // 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.scrollTimerInterval = this.options.scrollTimerInterval || 16; this.options.scrollBoundaries = Object.assign({ top: true, right: true, @@ -128,8 +73,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { this.options.jQueryDraggableOptions.scroll = false; const draggableOptions = Object.assign({}, this.options.jQueryDraggableOptions, { start: (ev, ui) => this._onSmoothDragStart(ev, ui, this.options.jQueryDraggableOptions.start), - drag: (ev, ui) => this._onSmoothDrag(ev, ui, this.options.jQueryDraggableOptions.drag), - stop: (ev, ui) => this._onSmoothDragStop(ev, ui, this.options.jQueryDraggableOptions.stop), + drag: (ev, ui, inst) => this._onSmoothDrag(ev, ui, this.options.jQueryDraggableOptions.drag), + stop: (ev, ui, inst) => this._onSmoothDragStop(ev, ui, this.options.jQueryDraggableOptions.stop), }); this.$element.draggable(draggableOptions); }, @@ -138,7 +83,6 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { */ destroy: function () { mixins.ParentedMixin.destroy.call(this); - this.$element.off('.smooth_scroll'); this._stopSmoothScroll(); }, @@ -159,56 +103,44 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { this._stopSmoothScroll(); 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 - }); + for (const scrollTarget of this.scrollableTargets) { + this._smoothScroll(ui, scrollTarget); } - this.$scrollTarget.scrollTop( - this.$scrollTarget.scrollTop() + - this.verticalDelta - ); - this.$scrollTarget.scrollLeft( - this.$scrollTarget.scrollLeft() + - this.horizontalDelta - ); }, - this.options.scrollTimerInterval + this.options.scrollTimerInterval, + ); + }, + _smoothScroll(ui, scrollTarget) { + const delta = this.deltaScrollableTargets.get(scrollTarget); + // Prevents Delta's from being different from 0 when scroll should not occur (except when + // helper is dragged outside of scrollTarget's visible area as it increases + // scrollTarget's scrollHeight). + // Also, this code prevents the helper from being incorrectly repositioned when target is + // a child of scrollTarget. + delta.vertical = Math.min( + // Ensures scrolling stops when dragging bellow scrollTarget bottom. + Math.max( 0, scrollTarget.scrollHeight - (scrollTarget.scrollTop + scrollTarget.clientHeight)), + // Ensures scrolling stops when dragging above scrollTarget top. + Math.max(delta.vertical, - scrollTarget.scrollTop) + ); + delta.horizontal = Math.min( + //Ensures scrolling stops when dragging left to scrollTarget. + Math.max(0, scrollTarget.scrollWidth - (scrollTarget.scrollLeft + scrollTarget.clientWidth)), + //Ensures scrolling stops when dragging right to scrollTarget. + Math.max(delta.horizontal, - scrollTarget.scrollLeft) + ); + + // Keep helper at right position while scrolling when helper is a child of scrollTarget. + const scrollTargetIsParent = scrollTarget.contains(this.$element.get(0)); + if (scrollTargetIsParent) { + ui.helper.css({ + 'margin-left': parseInt(ui.helper.css('margin-left') || 0) + delta.horizontal, + 'margin-top': parseInt(ui.helper.css('margin-top') || 0) + delta.vertical, + }); + } + scrollTarget.scrollTo( + scrollTarget.scrollLeft + delta.horizontal, + scrollTarget.scrollTop + delta.vertical, ); }, /** @@ -228,39 +160,29 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { * @private * @param {Object} ui The jQuery drag handler ui parameter. */ - _updatePositionOptions(ui) { - const draggableHelperOffset = ui.offset; - const scrollTargetOffset = this.$scrollTarget.offset(); + _getPositionDelta(ui, scrollTarget) { + const $scrollTarget = $(scrollTarget); + const scrollTargetIsDocument = $scrollTarget.is('html, body'); + const draggableHelperOffset = ui.helper[0].getBoundingClientRect(); + const scrollTargetOffset = scrollTarget.getBoundingClientRect(); + let visibleOffset = { top: draggableHelperOffset.top - scrollTargetOffset.top - + this.options.jQueryDraggableOptions.cursorAt.top - - this.options.offsetElementsManager.top(), - right: scrollTargetOffset.left + this.$scrollTarget.outerWidth() + + this.options.jQueryDraggableOptions.cursorAt.top, + right: scrollTargetOffset.left + + scrollTarget.clientWidth - draggableHelperOffset.left - - this.options.jQueryDraggableOptions.cursorAt.left - - this.options.offsetElementsManager.right(), - bottom: scrollTargetOffset.top + this.$scrollTarget.outerHeight() + - this.options.jQueryDraggableOptions.cursorAt.right, + bottom: scrollTargetOffset.top + + scrollTarget.clientHeight - draggableHelperOffset.top - - this.options.jQueryDraggableOptions.cursorAt.top - - this.options.offsetElementsManager.bottom(), + - this.options.jQueryDraggableOptions.cursorAt.bottom, left: draggableHelperOffset.left - scrollTargetOffset.left - + this.options.jQueryDraggableOptions.cursorAt.left - - this.options.offsetElementsManager.left(), + + this.options.jQueryDraggableOptions.cursorAt.left, }; - // If this.$scrollTarget is the html tag, we need to take the scroll position in to account - // as offsets positions are calculated relative to the document (thus ). - if (this.scrollTargetIsDocument) { - 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, @@ -304,35 +226,26 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { } } - 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))); + return { + vertical: Math.ceil(scrollStepDirection.vertical * + this.options.scrollStep * + (1 - Math.sqrt(scrollDecelerator.vertical))), + horizontal: 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. + * When use dropzones options, call 'over' option when the helper is + * over a dropzone and call 'out' when the helper is no longer above + * the dropzone. * * @private * @param {Object} ev The jQuery drag handler event parameter. @@ -340,10 +253,59 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { * @param {Function} onDragCallback The jQuery drag callback. */ _onSmoothDrag(ev, ui, onDragCallback) { - this._updatePositionOptions(ui); + for (const scrollTarget of this.scrollableTargets) { + const delta = this._getPositionDelta(ui, scrollTarget); + this.deltaScrollableTargets.set(scrollTarget, delta); + } + if (typeof onDragCallback === 'function') { onDragCallback.call(ui.helper, ev, ui); } + + if (this.options.dropzones) { + if (!this.$dropzones) { + this.$dropzones = this.options.dropzones.call(ui.helper, ev, ui).filter(function () { + if (ui.helper[0].contains(this)) { + console.warn('One drop zone is include inside the draggable item.'); + } else { + return true; + } + }); + } + if (!this.$dropzones.length) { + return; + } + const draggableBox = ui.helper[0].getBoundingClientRect(); + + let over; + for (const zone of this.$dropzones.get()) { + let box; + if (this.overDropzone && this.overDropzone[0] === zone) { + box = this.overDropzone[1]; + } else { + box = zone.getBoundingClientRect(); + } + if (box.top < draggableBox.top + draggableBox.height && box.top + box.height > draggableBox.top && + box.left < draggableBox.left + draggableBox.width && box.left + box.width > draggableBox.left) { + over = zone; + break; + } + } + if (this.overDropzone && this.overDropzone[0] !== over) { + this.options.out.call(ui.helper, ui, this.overDropzone[0]); + this.overDropzone = null; + } + if (over && !this.overDropzone) { + const box = over.getBoundingClientRect(); + this.overDropzone = [over, { + top: box.top, + height: box.height, + left: box.left, + width: box.width, + }]; + this.options.over.call(ui.helper, ui, over); + } + } }, /** * Called when starting to drag the element. @@ -356,9 +318,34 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { * @param {Function} onDragStartCallBack The jQuery drag callback. */ _onSmoothDragStart(ev, ui, onDragStartCallBack) { - this.scrollTargetIsDocument = this.$scrollTarget.is('html'); - this.scrollTargetIsParent = this.$scrollTarget.get(0).contains(this.$element.get(0)); - this._updatePositionOptions(ui); + const elementOffset = ev.target.getBoundingClientRect(); + this.options.jQueryDraggableOptions.cursorAt = { + top: ev.pageY - elementOffset.top, + right: elementOffset.right - ev.pageX, + bottom: elementOffset.bottom - ev.pageY, + left: ev.pageX - elementOffset.left, + }; + + this.scrollableTargets = []; + this.deltaScrollableTargets = new WeakMap(); + let scrollTarget = this.$container[0]; + while (scrollTarget) { + if (scrollTarget.nodeType === 1 && scrollTarget.scrollHeight > scrollTarget.clientHeight) { + const overflow = window.getComputedStyle(scrollTarget).overflow; + if (overflow.includes('auto') || overflow.includes('scroll')) { + this.scrollableTargets.push(scrollTarget); + this.deltaScrollableTargets.set(scrollTarget, { + vertical: 0, + horizontal: 0, + }); + } + } + scrollTarget = scrollTarget.parentNode; + if (scrollTarget && scrollTarget.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scrollTarget.host) { + scrollTarget = scrollTarget.host; + } + } + this._startSmoothScroll(ui); if (typeof onDragStartCallBack === 'function') { onDragStartCallBack.call(ui.helper, ev, ui); @@ -378,6 +365,8 @@ const SmoothScrollOnDrag = Class.extend(mixins.ParentedMixin, { if (typeof onDragEndCallBack === 'function') { onDragEndCallBack.call(ui.helper, ev, ui); } + this.$dropzones = null; + this.overDropzone = null; }, }); diff --git a/addons/web_editor/static/src/js/backend/field_html.js b/addons/web_editor/static/src/js/backend/field_html.js index 7005318ef28..6b39ec7bf16 100644 --- a/addons/web_editor/static/src/js/backend/field_html.js +++ b/addons/web_editor/static/src/js/backend/field_html.js @@ -3,9 +3,8 @@ odoo.define('web_editor.field.html', function (require) { var ajax = require('web.ajax'); var basic_fields = require('web.basic_fields'); -var config = require('web.config'); var core = require('web.core'); -var Wysiwyg = require('web_editor.wysiwyg.root'); +var wysiwygLoader = require('web_editor.loader'); var field_registry = require('web.field_registry'); // must wait for web/ to add the default html widget, otherwise it would override the web_editor one require('web._field_registry'); @@ -13,7 +12,6 @@ require('web._field_registry'); var _lt = core._lt; var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin; var QWeb = core.qweb; -var assetsLoaded; var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/; @@ -32,7 +30,7 @@ var jinjaRegex = /(^|\n)\s*%\s(end|set\s)/; */ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { description: _lt("Html"), - className: 'oe_form_field oe_form_field_html', + className: 'oe_form_field oe_form_field_html d-flex', supportedFieldTypes: ['html'], custom_events: { @@ -45,32 +43,20 @@ var FieldHtml = basic_fields.DebouncedField.extend(TranslatableFieldMixin, { /** * @override */ - willStart: function () { - var self = this; - this.isRendered = false; - this._onUpdateIframeId = 'onLoad_' + _.uniqueId('FieldHtml'); - var defAsset; + willStart: async function () { + await this._super(); if (this.nodeOptions.cssReadonly) { - defAsset = ajax.loadAsset(this.nodeOptions.cssReadonly); + this.cssReadonly = await ajax.loadAsset(this.nodeOptions.cssReadonly); } - - if (!assetsLoaded) { // avoid flickering when begin to edit - assetsLoaded = new Promise(function (resolve) { - var wysiwyg = new Wysiwyg(self, {}); - wysiwyg.attachTo($('