summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/components
diff options
context:
space:
mode:
authorJulien Mougenot <jum@odoo.com>2020-03-11 09:38:28 +0000
committerJulien Mougenot <jum@odoo.com>2020-03-13 14:08:33 +0000
commitfbf347498f1cc7b74ef373179b7bcae201715c24 (patch)
treeb8d1eb29ec93b6c2e43dbade712e636d32fe6716 /addons/web/static/src/js/components
parent0a129284e4856b6485c48a194fd86c3ab5c736e7 (diff)
[REF] web: Control panel Owl
Refactoring of the control panel with the Owl framework. The MVC structure of the control panel has been changed to a 'component <-> model' relationship. As such: - Removed the old control panel: view/model/controller/renderer - Created a new Owl Component (ControlPanel) - Created a new class: the Model (an Owl Store did not fit here) and a newly designed ControlPanelModel extending it Refactored all inner subcomponents, including: - Pager - ActionMenus (peviously SideBar) - Filter menu > Custom filter item - Group by menu > Custom group by item > Removal of groupby_menu_mixin (and refactored graph controller in sibling commit) - Time range menu - Favorite menu > Custom favorite item > Moved the favorite menu registry to the favorite_menu file - Search bar > Removed and splitted auto-completion dropdown logic between the new SearchBar Component (descriptions) and to the new ControlPanelModel (domains). The templates (all in web/base.xml) have been adapted to the new standards. Most of the DOM structure remains unchanged with some occasional improvements (replace hiding with not rendering, removed pointless wrapper divs, etc.). Part of task 2196029 Co-authored-by: Mathieu Duckerts-Antoine <dam@odoo.com>
Diffstat (limited to 'addons/web/static/src/js/components')
-rw-r--r--addons/web/static/src/js/components/action_menus.js171
-rw-r--r--addons/web/static/src/js/components/datepicker.js244
-rw-r--r--addons/web/static/src/js/components/dropdown_menu.js133
-rw-r--r--addons/web/static/src/js/components/dropdown_menu_item.js98
-rw-r--r--addons/web/static/src/js/components/pager.js221
5 files changed, 867 insertions, 0 deletions
diff --git a/addons/web/static/src/js/components/action_menus.js b/addons/web/static/src/js/components/action_menus.js
new file mode 100644
index 00000000000..d94e162ca44
--- /dev/null
+++ b/addons/web/static/src/js/components/action_menus.js
@@ -0,0 +1,171 @@
+odoo.define('web.ActionMenus', function (require) {
+ "use strict";
+
+ const Context = require('web.Context');
+ const DropdownMenu = require('web.DropdownMenu');
+ const pyUtils = require('web.py_utils');
+ const Registry = require('web.Registry');
+
+ const { Component } = owl;
+
+ /**
+ * Action menus (or Action/Print bar, previously called 'Sidebar')
+ *
+ * The side bar is the group of dropdown menus located on the left side of the
+ * control panel. Its role is to display a list of items depending on the view
+ * type and selected records and to execute a set of actions on active records.
+ * It is made out of 2 dropdown menus: Print and Action.
+ *
+ * This component also provides a registry to use custom components in the ActionMenus's
+ * Action menu.
+ * @extends Component
+ */
+ class ActionMenus extends Component {
+
+ mounted() {
+ this._addTooltips();
+ }
+
+ patched() {
+ this._addTooltips();
+ }
+
+ //---------------------------------------------------------------------
+ // Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * @returns {Object[]}
+ */
+ get actionItems() {
+ // Callback based actions
+ const callbackActions = (this.props.items.other || []).map(
+ action => Object.assign({ key: `action-${action.description}` }, action)
+ );
+ // Action based actions
+ const actionActions = this.props.items.action || [];
+ const relateActions = this.props.items.relate || [];
+ const formattedActions = [...actionActions, ...relateActions].map(
+ action => ({ action, description: action.name, key: action.id })
+ );
+ // ActionMenus action registry components
+ const registryActions = this.constructor.registry.values().map(
+ ({ Component, getProps }, index) => ({
+ key: `registry-action-${index}`,
+ Component,
+ props: getProps(this.props),
+ })
+ );
+ return [...callbackActions, ...formattedActions, ...registryActions];
+ }
+
+ /**
+ * @returns {Object[]}
+ */
+ get printItems() {
+ const printActions = this.props.items.print || [];
+ const printItems = printActions.map(
+ action => ({ action, description: action.name, key: action.id })
+ );
+ return printItems;
+ }
+
+ //---------------------------------------------------------------------
+ // Private
+ //---------------------------------------------------------------------
+
+ /**
+ * Add teh tooltips to the items
+ * @private
+ */
+ _addTooltips() {
+ $(this.el.querySelectorAll('[title]')).tooltip({
+ delay: { show: 500, hide: 0 }
+ });
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * Perform the action for the item clicked after getting the data
+ * necessary with a trigger.
+ * @private
+ * @param {OwlEvent} ev
+ */
+ async _executeAction(action) {
+ const activeIdsContext = {
+ active_id: this.props.activeIds[0],
+ active_ids: this.props.activeIds,
+ active_model: this.env.action.res_model,
+ select_all: this.props.selectAll,
+ };
+ if (this.props.domain) {
+ activeIdsContext.active_domain = this.props.domain;
+ }
+
+ const context = pyUtils.eval('context', new Context(this.props.context, activeIdsContext));
+ const result = await this.rpc({
+ route: '/web/action/load',
+ params: {
+ action_id: action.id,
+ context: context,
+ },
+ });
+ result.context = new Context(result.context || {}, activeIdsContext)
+ .set_eval_context(context);
+ result.flags = result.flags || {};
+ result.flags.new_window = true;
+ this.trigger('do-action', {
+ action: result,
+ options: {
+ on_close: () => this.trigger('reload'),
+ },
+ });
+ }
+
+ /**
+ * Handler used to determine which way must be used to execute a selected
+ * action: it will be either:
+ * - a callback (function given by the view controller);
+ * - an action ID (string);
+ * - an URL (string).
+ * @private
+ * @param {OwlEvent} ev
+ */
+ _onItemSelected(ev) {
+ ev.stopPropagation();
+ const { item } = ev.detail;
+ if (item.callback) {
+ item.callback([item]);
+ } else if (item.action) {
+ this._executeAction(item.action);
+ } else if (item.url) {
+ // Event has been prevented at its source: we need to redirect manually.
+ this.env.services.navigate(item.url);
+ }
+ }
+ }
+
+ ActionMenus.registry = new Registry();
+
+ ActionMenus.components = { DropdownMenu };
+ ActionMenus.props = {
+ activeIds: { type: Array, element: [Number|String] }, // virtual IDs are strings.
+ context: Object,
+ domain: { type: Array, optional: 1 },
+ items: {
+ type: Object,
+ shape: {
+ action: { type: Array, optional: 1 },
+ print: { type: Array, optional: 1 },
+ other: { type: Array, optional: 1 },
+ },
+ },
+ selectAll: { type: Boolean, optional: 1 },
+ };
+ ActionMenus.template = 'web.ActionMenus';
+
+ return ActionMenus;
+});
diff --git a/addons/web/static/src/js/components/datepicker.js b/addons/web/static/src/js/components/datepicker.js
new file mode 100644
index 00000000000..715e594e077
--- /dev/null
+++ b/addons/web/static/src/js/components/datepicker.js
@@ -0,0 +1,244 @@
+odoo.define('web.DatePickerOwl', function (require) {
+ "use strict";
+
+ const field_utils = require('web.field_utils');
+ const time = require('web.time');
+ const { useAutofocus } = require('web.custom_hooks');
+
+ const { Component, hooks } = owl;
+ const { useExternalListener, useRef, useState } = hooks;
+
+ let datePickerId = 0;
+
+ /**
+ * Date picker
+ *
+ * This component exposes the API of the tempusdominus datepicker library.
+ * As such, its template is a simple input that will open the TD datepicker
+ * when clicked on. The component will also synchronize any user-input value
+ * with the library widget and vice-vera.
+ *
+ * For further details regarding the implementation of the picker itself, please
+ * refer to the official tempusdominus documentation (note: all props given
+ * to this component will be passed as arguments to instantiate the picker widget).
+ * @extends Component
+ */
+ class DatePicker extends Component {
+ constructor() {
+ super(...arguments);
+
+ this.inputRef = useRef('input');
+ this.state = useState({ warning: false });
+
+ this.datePickerId = `o_datepicker_${datePickerId++}`;
+ this.typeOfDate = 'date';
+
+ useAutofocus();
+ useExternalListener(window, 'scroll', this._onWindowScroll);
+ }
+
+ mounted() {
+ $(this.el).on('show.datetimepicker', this._onDateTimePickerShow.bind(this));
+ $(this.el).on('hide.datetimepicker', this._onDateTimePickerHide.bind(this));
+ $(this.el).on('error.datetimepicker', () => false);
+
+ this._datetimepicker(this.props);
+ this.inputRef.el.value = this._formatDate(this.props.date);
+ }
+
+ willUnmount() {
+ this._datetimepicker('destroy');
+ }
+
+ willUpdateProps(nextProps) {
+ if (nextProps.date) {
+ this._datetimepicker('date', nextProps.date);
+ this.inputRef.el.value = this._formatDate(nextProps.date);
+ }
+ }
+
+ //---------------------------------------------------------------------
+ // Private
+ //---------------------------------------------------------------------
+
+ /**
+ * Handle bootstrap datetimepicker calls.
+ * @private
+ * @param {...any} args anything that will be passed to the datetimepicker function.
+ */
+ _datetimepicker(...args) {
+ $(this.el).datetimepicker(...args);
+ }
+
+ /**
+ * @private
+ * @param {moment} date
+ * @returns {string}
+ */
+ _formatDate(date) {
+ try {
+ return field_utils.format[this.typeOfDate](date, null, { timezone: false });
+ } catch (err) {
+ return false;
+ }
+ }
+
+ /**
+ * @private
+ * @param {string|false} value
+ * @returns {moment}
+ */
+ _parseInput(inputValue) {
+ try {
+ return field_utils.parse[this.typeOfDate](inputValue, null, { timezone: false });
+ } catch (err) {
+ return false;
+ }
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * Reacts to the datetimepicker being hidden
+ * Used to unbind the scroll event from the datetimepicker
+ * @private
+ */
+ _onDateTimePickerHide() {
+ const date = this._parseInput(this.inputRef.el.value);
+ this.state.warning = date.format('YYYY-MM-DD') > moment().format('YYYY-MM-DD');
+ this.trigger('datetime-changed', { date });
+ }
+
+ /**
+ * Reacts to the datetimepicker being shown
+ * Could set/verify our widget value
+ * And subsequently update the datetimepicker
+ * @private
+ */
+ _onDateTimePickerShow() {
+ this.inputRef.el.select();
+ }
+
+ /**
+ * @private
+ */
+ _onInputClick() {
+ this._datetimepicker('toggle');
+ }
+
+ /**
+ * @private
+ */
+ _onInputChange() {
+ const date = this._parseInput(this.inputRef.el.value);
+ if (date) {
+ this.state.warning = date.format('YYYY-MM-DD') > moment().format('YYYY-MM-DD');
+ this.trigger('datetime-changed', { date });
+ } else {
+ this.inputRef.el.value = this._formatDate(this.props.date);
+ }
+ }
+
+ /**
+ * @private
+ */
+ _onWindowScroll(ev) {
+ if (ev.target !== this.inputRef.el) {
+ this._datetimepicker('hide');
+ }
+ }
+ }
+
+ DatePicker.defaultProps = {
+ calendarWeeks: true,
+ format: time.getLangDateFormat(),
+ icons: {
+ clear: 'fa fa-delete',
+ close: 'fa fa-check primary',
+ date: 'fa fa-calendar',
+ down: 'fa fa-chevron-down',
+ next: 'fa fa-chevron-right',
+ previous: 'fa fa-chevron-left',
+ time: 'fa fa-clock-o',
+ today: 'fa fa-calendar-check-o',
+ up: 'fa fa-chevron-up',
+ },
+ locale: moment.locale(),
+ maxDate: moment({ y: 9999, M: 11, d: 31 }),
+ minDate: moment({ y: 1900 }),
+ useCurrent: false,
+ widgetParent: 'body',
+ };
+ DatePicker.props = {
+ // Actual date value
+ date: moment,
+ // Other props
+ buttons: {
+ type: Object,
+ shape: {
+ showClear: Boolean,
+ showClose: Boolean,
+ showToday: Boolean,
+ },
+ optional: 1,
+ },
+ calendarWeeks: Boolean,
+ format: String,
+ icons: {
+ type: Object,
+ shape: {
+ clear: String,
+ close: String,
+ date: String,
+ down: String,
+ next: String,
+ previous: String,
+ time: String,
+ today: String,
+ up: String,
+ },
+ },
+ keyBinds: { validate: kb => typeof kb === 'object' || kb === null, optional: 1 },
+ locale: String,
+ maxDate: moment,
+ minDate: moment,
+ readonly: { type: Boolean, optional: 1 },
+ useCurrent: Boolean,
+ widgetParent: String,
+ };
+ DatePicker.template = 'web.DatePicker';
+
+ /**
+ * Date/time picker
+ *
+ * Similar to the DatePicker component, adding the handling of more specific
+ * time values: hour-minute-second.
+ *
+ * Once again, refer to the tempusdominus documentation for implementation
+ * details.
+ * @extends DatePicker
+ */
+ class DateTimePicker extends DatePicker {
+ constructor() {
+ super(...arguments);
+
+ this.typeOfDate = 'datetime';
+ }
+ }
+
+ DateTimePicker.defaultProps = Object.assign({}, DatePicker.defaultProps, {
+ buttons: {
+ showClear: false,
+ showClose: true,
+ showToday: false,
+ },
+ format: time.getLangDatetimeFormat(),
+ });
+
+ return {
+ DatePicker,
+ DateTimePicker,
+ };
+});
diff --git a/addons/web/static/src/js/components/dropdown_menu.js b/addons/web/static/src/js/components/dropdown_menu.js
new file mode 100644
index 00000000000..df7172ae1d4
--- /dev/null
+++ b/addons/web/static/src/js/components/dropdown_menu.js
@@ -0,0 +1,133 @@
+odoo.define('web.DropdownMenu', function (require) {
+ "use strict";
+
+ const DropdownMenuItem = require('web.DropdownMenuItem');
+
+ const { Component, hooks } = owl;
+ const { useExternalListener, useRef, useState } = hooks;
+
+ /**
+ * Dropdown menu
+ *
+ * Generic component used to generate a list of interactive items. It uses some
+ * bootstrap classes but most interactions are handled in here or in the dropdown
+ * menu item class definition, including some keyboard navigation and escaping
+ * system (click outside to close the dropdown).
+ *
+ * The layout of a dropdown menu is as following:
+ * > a Button (always rendered) with a `title` and an optional `icon`;
+ * > a Dropdown (rendered when open) containing a collection of given items.
+ * These items must be objects and can have two shapes:
+ * 1. item.Component & item.props > will instantiate the given Component with
+ * the given props. Any additional key will be useless.
+ * 2. any other shape > will instantiate a DropdownMenuItem with the item
+ * object being its props. There is no props validation as this object
+ * will be passed as-is when `selected` and can contain additional meta-keys
+ * that will not affect the displayed item. For more information regarding
+ * the behaviour of these items, @see DropdownMenuItem.
+ * @extends Component
+ */
+ class DropdownMenu extends Component {
+ constructor() {
+ super(...arguments);
+
+ this.dropdownMenu = useRef('dropdown');
+ this.state = useState({ open: false });
+
+ this.symbol = this.env.device.isMobile ? 'fa fa-chevron-right float-right mt4' : false;
+
+ useExternalListener(window, 'click', this._onWindowClick);
+ useExternalListener(window, 'keydown', this._onWindowKeydown);
+ }
+
+ //---------------------------------------------------------------------
+ // Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * Overriden in case we want to keep the caret style on the button in mobile.
+ * @returns {boolean}
+ */
+ get displayCaret() {
+ return !this.env.device.isMobile;
+ }
+
+ /**
+ * Meant to be overriden to provide the list of items to display.
+ * @returns {Object[]}
+ */
+ get items() {
+ return this.props.items;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get title() {
+ return this.props.title;
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onButtonKeydown(ev) {
+ switch (ev.key) {
+ case 'ArrowLeft':
+ case 'ArrowRight':
+ case 'ArrowUp':
+ case 'ArrowDown':
+ const firstItem = this.el.querySelector('.dropdown-item');
+ if (firstItem) {
+ ev.preventDefault();
+ firstItem.focus();
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {OwlEvent} ev
+ */
+ _onItemSelected(ev) { }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onWindowClick(ev) {
+ if (this.state.open && !this.el.contains(ev.target)) {
+ this.state.open = false;
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onWindowKeydown(ev) {
+ if (this.state.open && ev.key === 'Escape') {
+ this.state.open = false;
+ }
+ }
+ }
+
+ DropdownMenu.components = { DropdownMenuItem };
+ DropdownMenu.defaultProps = { items: [] };
+ DropdownMenu.props = {
+ icon: { type: String, optional: 1 },
+ items: {
+ type: Array,
+ element: Object,
+ optional: 1,
+ },
+ title: { type: String, optional: 1 },
+ };
+ DropdownMenu.template = 'web.DropdownMenu';
+
+ return DropdownMenu;
+});
diff --git a/addons/web/static/src/js/components/dropdown_menu_item.js b/addons/web/static/src/js/components/dropdown_menu_item.js
new file mode 100644
index 00000000000..bc028e17719
--- /dev/null
+++ b/addons/web/static/src/js/components/dropdown_menu_item.js
@@ -0,0 +1,98 @@
+odoo.define('web.DropdownMenuItem', function (require) {
+ "use strict";
+
+ const { useListener } = require('web.custom_hooks');
+
+ const { Component, hooks } = owl;
+ const { useExternalListener, useRef, useState } = hooks;
+
+ /**
+ * Dropdown menu item
+ *
+ * Generic component instantiated by a dropdown menu (@see DropdownMenu) in
+ * the absence of `Component` and `props` keys in a given item object.
+ *
+ * In its simplest form, a dropdown menu item will be given a description (optional,
+ * but highly recommended) and will trigger a 'select-item' when clicked on.
+ * Additionaly it can receive the following props:
+ * - isActive: will add a `checked` symbol on the left side of the item
+ * - removable: will add a `remove` trash icon on the right side of the item.
+ * when clicked, will trigger a 'remove-item' event.
+ * - options: will change the behaviour of the item ; instead of triggering
+ * an event, the item will act as a nested dropdown menu and display
+ * its given options. These will have the same definition as another
+ * dropdown item but cannot have options of their own.
+ *
+ * It is recommended to extend this class when defining a Component which will
+ * be put inside of a dropdown menu (@see CustomFilterItem as example).
+ * @extends Component
+ */
+ class DropdownMenuItem extends Component {
+ constructor() {
+ super(...arguments);
+
+ this.canBeOpened = Boolean(this.props.options && this.props.options.length);
+
+ this.fallbackFocusRef = useRef('fallback-focus');
+ this.state = useState({ open: false });
+
+ useExternalListener(window, 'click', this._onWindowClick);
+ useListener('keydown', this._onKeydown);
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onKeydown(ev) {
+ if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) {
+ return;
+ }
+ switch (ev.key) {
+ case 'ArrowLeft':
+ if (this.canBeOpened && this.state.open) {
+ ev.preventDefault();
+ if (this.fallbackFocusRef.el) {
+ this.fallbackFocusRef.el.focus();
+ }
+ this.state.open = false;
+ }
+ break;
+ case 'ArrowRight':
+ if (this.canBeOpened && !this.state.open) {
+ ev.preventDefault();
+ this.state.open = true;
+ }
+ break;
+ case 'Escape':
+ ev.target.blur();
+ if (this.canBeOpened && this.state.open) {
+ ev.preventDefault();
+ ev.stopPropagation();
+ if (this.fallbackFocusRef.el) {
+ this.fallbackFocusRef.el.focus();
+ }
+ this.state.open = false;
+ }
+ }
+ }
+
+ /**
+ * @private
+ * @param {MouseEvent} ev
+ */
+ _onWindowClick(ev) {
+ if (this.state.open && !this.el.contains(ev.target)) {
+ this.state.open = false;
+ }
+ }
+ }
+
+ DropdownMenuItem.template = 'web.DropdownMenuItem';
+
+ return DropdownMenuItem;
+});
diff --git a/addons/web/static/src/js/components/pager.js b/addons/web/static/src/js/components/pager.js
new file mode 100644
index 00000000000..cada405ac7d
--- /dev/null
+++ b/addons/web/static/src/js/components/pager.js
@@ -0,0 +1,221 @@
+odoo.define('web.Pager', function (require) {
+ "use strict";
+
+ const { useAutofocus } = require('web.custom_hooks');
+
+ const { Component, hooks } = owl;
+ const { useState } = hooks;
+
+ /**
+ * Pager
+ *
+ * The pager goes from 1 to size (included).
+ * The current value is currentMinimum if limit === 1 or the interval:
+ * [currentMinimum, currentMinimum + limit[ if limit > 1].
+ * The value can be manually changed by clicking on the pager value and giving
+ * an input matching the pattern: min[,max] (in which the comma can be a dash
+ * or a semicolon).
+ * The pager also provides two buttons to quickly change the current page (next
+ * or previous).
+ * @extends Component
+ */
+ class Pager extends Component {
+ /**
+ * @param {Object} [props]
+ * @param {int} [props.size] the total number of elements
+ * @param {int} [props.currentMinimum] the first element of the current_page
+ * @param {int} [props.limit] the number of elements per page
+ * @param {boolean} [props.editable] editable feature of the pager
+ * @param {function} [props.validate] callback returning a Promise to
+ * validate changes
+ * @param {boolean} [props.withAccessKey] can be disabled, for example,
+ * for x2m widgets
+ */
+ constructor() {
+ super(...arguments);
+
+ this.state = useState({
+ disabled: false,
+ editing: false,
+ });
+
+ useAutofocus();
+ }
+
+ async willUpdateProps(nextProps) {
+ this.state.editing = false;
+ if (
+ this.props.currentMinimum !== nextProps.currentMinimum ||
+ this.props.limit !== nextProps.limit ||
+ this.props.size !== nextProps.size
+ ) {
+ this.state.disabled = false;
+ }
+ }
+
+ //---------------------------------------------------------------------
+ // Getters
+ //---------------------------------------------------------------------
+
+ /**
+ * @returns {number}
+ */
+ get maximum() {
+ return Math.min(this.props.currentMinimum + this.props.limit - 1, this.props.size);
+ }
+
+ /**
+ * @returns {boolean} true iff there is only one page
+ */
+ get singlePage() {
+ const { currentMinimum, size } = this.props;
+ return (1 === currentMinimum) && (this.maximum === size);
+ }
+
+ /**
+ * @returns {number}
+ */
+ get value() {
+ return this.props.currentMinimum + (this.props.limit > 1 ? `-${this.maximum}` : '');
+ }
+
+ //---------------------------------------------------------------------
+ // Private
+ //---------------------------------------------------------------------
+
+ /**
+ * Update the pager's state according to a pager action
+ * @private
+ * @param {number} [direction] the action (previous or next) on the pager
+ */
+ async _changeSelection(direction) {
+ try {
+ await this.props.validate();
+ } catch (err) {
+ return;
+ }
+ const { limit, size } = this.props;
+
+ // Compute the new currentMinimum
+ let currentMinimum = (this.props.currentMinimum + limit * direction);
+ if (currentMinimum > size) {
+ currentMinimum = 1;
+ } else if ((currentMinimum < 1) && (limit === 1)) {
+ currentMinimum = size;
+ } else if ((currentMinimum < 1) && (limit > 1)) {
+ currentMinimum = size - ((size % limit) || limit) + 1;
+ }
+
+ // The re-rendering of the pager must be done before the trigger of
+ // event 'pager-changed' as the rendering may enable the pager
+ // (and a common use is to disable the pager when this event is
+ // triggered, and to re-enable it when the data have been reloaded).
+ this._updateAndDisable(currentMinimum, limit);
+ }
+
+ /**
+ * Save the state from the content of the input
+ * @private
+ * @param {string} value the new raw pager value
+ * @returns {Promise}
+ */
+ async _saveValue(value) {
+ try {
+ await this.props.validate();
+ } catch (err) {
+ return;
+ }
+ const [min, max] = value.trim().split(/\s*[\-\s,;]\s*/);
+
+ let currentMinimum = Math.max(Math.min(parseInt(min, 10), this.props.size), 1);
+ let maximum = max ? Math.max(Math.min(parseInt(max, 10), this.props.size), 1) : min;
+
+ if (
+ !isNaN(currentMinimum) &&
+ !isNaN(maximum) &&
+ currentMinimum <= maximum
+ ) {
+ const limit = Math.max(maximum - currentMinimum) + 1;
+ this._updateAndDisable(currentMinimum, limit);
+ }
+ }
+
+ /**
+ * @private
+ * @param {Object} payload
+ */
+ _updateAndDisable(currentMinimum, limit) {
+ if (
+ currentMinimum !== this.props.currentMinimum ||
+ limit !== this.props.limit
+ ) {
+ this.state.disabled = true;
+ this.trigger('pager-changed', { currentMinimum, limit });
+ }
+ }
+
+ //---------------------------------------------------------------------
+ // Handlers
+ //---------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onEdit() {
+ if (
+ this.props.editable && // editable
+ !this.state.editing && // not already editing
+ !this.state.disabled // not being changed already
+ ) {
+ this.state.editing = true;
+ }
+ }
+
+ /**
+ * @private
+ * @param {InputEvent} ev
+ */
+ _onValueChange(ev) {
+ this._saveValue(ev.currentTarget.value);
+ if (!this.state.disabled) {
+ ev.preventDefault();
+ }
+ }
+
+ /**
+ * @private
+ * @param {KeyboardEvent} ev
+ */
+ _onValueKeydown(ev) {
+ switch (ev.key) {
+ case 'Enter':
+ ev.preventDefault();
+ ev.stopPropagation();
+ this._saveValue(ev.currentTarget.value);
+ break;
+ case 'Escape':
+ ev.preventDefault();
+ ev.stopPropagation();
+ this.state.editing = false;
+ break;
+ }
+ }
+ }
+
+ Pager.defaultProps = {
+ editable: true,
+ validate: async () => { },
+ withAccessKey: true,
+ };
+ Pager.props = {
+ currentMinimum: { type: Number, optional: 1 },
+ editable: Boolean,
+ limit: { validate: l => !isNaN(l), optional: 1 },
+ size: { type: Number, optional: 1 },
+ validate: Function,
+ withAccessKey: Boolean,
+ };
+ Pager.template = 'web.Pager';
+
+ return Pager;
+});