summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/components
diff options
context:
space:
mode:
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;
+});