summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/core
diff options
context:
space:
mode:
authorJulien Mougenot <jum@odoo.com>2019-11-22 11:36:52 +0000
committerJulien Mougenot <jum@odoo.com>2020-01-06 14:37:16 +0000
commit34bbdd88c0bc354b76b254bb78c4eb786f3d08c0 (patch)
treebb5b17494f521fb9a4a50869d32040273d1d5d47 /addons/web/static/src/js/core
parentab37648cb1c3a4ab7db23e3a8cdef87008d5d1e0 (diff)
[ADD] web: Owl Dialog
New Owl component: Dialog. It does not rely on Bootstrap but behaves the same and does not conflict with legacy dialogs. To call this component, you need to define it in the XML parent template instead of calling it in the JS file, and put a flag in the parent state to toggle the dialog. Old dialog file has also been slightly updated to ensure compatibility between both versions. This commit also updates a list view test which did not wait for click events to properly trigger and would cause the new dialog system to crash.
Diffstat (limited to 'addons/web/static/src/js/core')
-rw-r--r--addons/web/static/src/js/core/dialog.js7
-rw-r--r--addons/web/static/src/js/core/owl_dialog.js259
2 files changed, 266 insertions, 0 deletions
diff --git a/addons/web/static/src/js/core/dialog.js b/addons/web/static/src/js/core/dialog.js
index bb522992c2f..6e9620421c3 100644
--- a/addons/web/static/src/js/core/dialog.js
+++ b/addons/web/static/src/js/core/dialog.js
@@ -4,6 +4,7 @@ odoo.define('web.Dialog', function (require) {
var core = require('web.core');
var dom = require('web.dom');
var Widget = require('web.Widget');
+const OwlDialog = require('web.OwlDialog');
var QWeb = core.qweb;
var _t = core._t;
@@ -205,6 +206,9 @@ var Dialog = Widget.extend({
if (options && options.shouldFocusButtons) {
self._onFocusControlButton();
}
+
+ // Notifies OwlDialog to adjust focus/active properties on owl dialogs
+ OwlDialog.display(self);
});
return self;
@@ -236,6 +240,9 @@ var Dialog = Widget.extend({
return;
}
+ // Notifies OwlDialog to adjust focus/active properties on owl dialogs
+ OwlDialog.hide(this);
+
// Triggers the onForceClose event if the callback is defined
if (this.onForceClose) {
this.onForceClose();
diff --git a/addons/web/static/src/js/core/owl_dialog.js b/addons/web/static/src/js/core/owl_dialog.js
new file mode 100644
index 00000000000..a6ee3c7b1d8
--- /dev/null
+++ b/addons/web/static/src/js/core/owl_dialog.js
@@ -0,0 +1,259 @@
+odoo.define('web.OwlDialog', function (require) {
+ "use strict";
+
+ const { useExternalListener } = require('web.custom_hooks');
+
+ const { Component, hooks, misc } = owl;
+ const { Portal } = misc;
+ const { useRef } = hooks;
+ const SIZE_CLASSES = {
+ 'extra-large': 'modal-xl',
+ 'large': 'modal-lg',
+ 'small': 'modal-sm',
+ };
+
+ /**
+ * Dialog (owl version)
+ *
+ * Represents a bootstrap-styled dialog handled with pure JS. Its implementation
+ * is roughly the same as the legacy dialog, the only exception being the buttons.
+ * @extends Component
+ **/
+ class Dialog extends Component {
+ /**
+ * @param {Object} [props]
+ * @param {(boolean|string)} [props.backdrop='static'] The kind of modal backdrop
+ * to use (see Bootstrap documentation).
+ * @param {string} [props.contentClass] Class to add to the dialog
+ * @param {boolean} [props.fullscreen=false] Whether the dialog should be
+ * open in fullscreen mode (the main usecase is mobile).
+ * @param {boolean} [props.renderFooter=true] Whether the dialog footer
+ * should be rendered.
+ * @param {boolean} [props.renderHeader=true] Whether the dialog header
+ * should be rendered.
+ * @param {string} [props.size='large'] 'extra-large', 'large', 'medium'
+ * or 'small'.
+ * @param {string} [props.subtitle='']
+ * @param {string} [props.title='Odoo']
+ * @param {boolean} [props.technical=true] If set to false, the modal will have
+ * the standard frontend style (use this for non-editor frontend features).
+ */
+ constructor() {
+ super(...arguments);
+
+ this.modalRef = useRef('modal');
+ this.footerRef = useRef('modal-footer');
+
+ useExternalListener(window, 'keydown', this._onKeydown);
+ }
+
+ mounted() {
+ this.constructor.display(this);
+
+ this.env.bus.on('close_dialogs', this, this._close);
+
+ if (this.props.renderFooter) {
+ // Set up main button : will first look for an element with the
+ // 'btn-primary' class, then a 'btn' class, then the first button
+ // element.
+ let mainButton = this.footerRef.el.querySelector('.btn.btn-primary');
+ if (!mainButton) {
+ mainButton = this.footerRef.el.querySelector('.btn');
+ }
+ if (!mainButton) {
+ mainButton = this.footerRef.el.querySelector('button');
+ }
+ if (mainButton) {
+ this.mainButton = mainButton;
+ this.mainButton.addEventListener('keydown', this._onMainButtonKeydown.bind(this));
+ this.mainButton.focus();
+ }
+ }
+
+ this._removeTooltips();
+ }
+
+ async willUnmount() {
+ this.env.bus.off('close_dialogs', this, this._close);
+
+ this._removeTooltips();
+
+ this.constructor.hide(this);
+ }
+
+ //--------------------------------------------------------------------------
+ // Getters
+ //--------------------------------------------------------------------------
+
+ /**
+ * @returns {string}
+ */
+ get size() {
+ return SIZE_CLASSES[this.props.size];
+ }
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Send an event signaling that the dialog must be closed.
+ * @private
+ */
+ _close() {
+ this.trigger('dialog_closed');
+ }
+
+ /**
+ * Remove any existing tooltip present in the DOM.
+ * @private
+ */
+ _removeTooltips() {
+ for (const tooltip of document.querySelectorAll('.tooltip')) {
+ tooltip.remove(); // remove open tooltip if any to prevent them staying when modal is opened
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onBackdropClick() {
+ if (this.props.backdrop === 'static') {
+ if (this.mainButton) {
+ this.mainButton.focus();
+ }
+ } else {
+ this._close();
+ }
+ }
+
+ /**
+ * @private
+ */
+ _onFocus() {
+ if (this.mainButton) {
+ this.mainButton.focus();
+ }
+ }
+
+ /**
+ * Manage the TAB key on the main button. If the focus is on a primary
+ * button and the user tries to tab to go to the next button : a tooltip
+ * will be displayed.
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onMainButtonKeydown(ev) {
+ if (ev.key === 'Tab' && !ev.shiftKey) {
+ ev.preventDefault();
+ $(this.mainButton)
+ .tooltip({
+ delay: { show: 200, hide: 0 },
+ title: () => this.env.qweb.render('DialogButton.tooltip', {
+ title: this.mainButton.innerText.toUpperCase(),
+ }),
+ trigger: 'manual',
+ })
+ .tooltip('show');
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydown(ev) {
+ if (
+ ev.key === 'Escape' &&
+ !['INPUT', 'TEXTAREA'].includes(ev.target.tagName) &&
+ this.constructor.displayed[this.constructor.displayed.length - 1] === this
+ ) {
+ ev.preventDefault();
+ ev.stopImmediatePropagation();
+ ev.stopPropagation();
+ this._close();
+ }
+ }
+
+ //--------------------------------------------------------------------------
+ // Static
+ //--------------------------------------------------------------------------
+
+ /**
+ * Push the given dialog at the end of the displayed list then set it as
+ * active and all the others as passive.
+ * @param {(LegacyDialog|OwlDialog)} dialog
+ */
+ static display(dialog) {
+ // Deactivate previous dialog
+ const activeDialogEl = document.querySelector('.modal.o_active_modal');
+ if (activeDialogEl) {
+ activeDialogEl.classList.remove('o_active_modal');
+ }
+ // Push dialog
+ this.displayed.push(dialog);
+ // Add active class
+ const modalEl = dialog instanceof this ?
+ // Owl dialog
+ dialog.modalRef.el :
+ // Legacy dialog
+ dialog.$modal[0];
+ modalEl.classList.add('o_active_modal');
+ // Update body class
+ document.body.classList.add('modal-open');
+ }
+
+ /**
+ * Set the given displayed dialog as passive and the last added displayed dialog
+ * as active, then remove it from the displayed list.
+ * @param {(LegacyDialog|OwlDialog)} dialog
+ */
+ static hide(dialog) {
+ // Remove given dialog from the list
+ this.displayed.splice(this.displayed.indexOf(dialog), 1);
+ // Activate last dialog and update body class
+ const lastDialog = this.displayed[this.displayed.length - 1];
+ if (lastDialog) {
+ lastDialog.el.focus();
+ const modalEl = lastDialog instanceof this ?
+ // Owl dialog
+ lastDialog.modalRef.el :
+ // Legacy dialog
+ lastDialog.$modal[0];
+ modalEl.classList.add('o_active_modal');
+ } else {
+ document.body.classList.remove('modal-open');
+ }
+ }
+ }
+
+ Dialog.displayed = [];
+
+ Dialog.components = { Portal };
+ Dialog.defaultProps = {
+ backdrop: 'static',
+ renderFooter: true,
+ renderHeader: true,
+ size: 'large',
+ technical: true,
+ title: "Odoo",
+ };
+ Dialog.props = {
+ backdrop: { validate: b => ['static', true, false].includes(b), optional: 1 },
+ contentClass: { type: String, optional: 1 },
+ fullscreen: { type: Boolean, optional: 1 },
+ renderFooter: { type: Boolean, optional: 1 },
+ renderHeader: { type: Boolean, optional: 1 },
+ size: { validate: s => ['extra-large', 'large', 'medium', 'small'].includes(s), optional: 1 },
+ subtitle: { type: String, optional: 1 },
+ technical: { type: Boolean, optional: 1 },
+ title: { type: String, optional: 1 },
+ };
+ Dialog.template = 'OwlDialog';
+
+ return Dialog;
+});