diff --git a/addons/web/static/src/core/pager/pager.js b/addons/web/static/src/core/pager/pager.js new file mode 100644 index 00000000000..cfdfb00ce13 --- /dev/null +++ b/addons/web/static/src/core/pager/pager.js @@ -0,0 +1,168 @@ +/** @odoo-module **/ + +import { useAutofocus } from "../utils/hooks"; + +const { Component } = owl; +const { useExternalListener, useRef, useState } = owl.hooks; + +/** + * Pager + * + * The pager goes from 1 to total (included). + * The current value is minimum if limit === 1 or the interval: + * [minimum, minimum + 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 + */ +export class Pager extends Component { + setup() { + this.state = useState({ + isEditing: false, + isDisabled: false, + }); + + this.inputRef = useRef("input"); + useExternalListener(document, "mousedown", this.onClickAway, { capture: true }); + + this.forceFocus = useAutofocus(); + } + + /** + * @returns {number} + */ + get minimum() { + return this.props.offset + 1; + } + /** + * @returns {number} + */ + get maximum() { + return Math.min(this.props.offset + this.props.limit, this.props.total); + } + /** + * @returns {string} + */ + get value() { + const parts = [this.minimum]; + if (this.props.limit > 1) { + parts.push(this.maximum); + } + return parts.join("-"); + } + /** + * @returns {boolean} true iff there is only one page + */ + get isSinglePage() { + return this.minimum === 1 && this.maximum === this.props.total; + } + + /** + * @param {-1 | 1} direction + */ + navigate(direction) { + let minimum = this.props.offset + this.props.limit * direction; + if (minimum >= this.props.total) { + minimum = 0; + } else if (minimum < 0 && this.props.limit === 1) { + minimum = this.props.total - 1; + } else if (minimum < 0 && this.props.limit > 1) { + minimum = this.props.total - (this.props.total % this.props.limit || this.props.limit); + } + + this.update(minimum, this.props.limit); + } + /** + * @param {string} value + * @returns {{ minimum: number, maximum: number }} + */ + parse(value) { + let [minimum, maximum] = value.trim().split(/\s*[\-\s,;]\s*/); + const clamp = (value) => Math.min(Math.max(value, 1), this.props.total); + return { + minimum: clamp(parseInt(minimum, 10)) - 1, + maximum: clamp(maximum ? parseInt(maximum, 10) : minimum), + }; + } + /** + * @param {string} value + */ + setValue(value) { + const { minimum, maximum } = this.parse(value); + + if (!isNaN(minimum) && !isNaN(maximum) && minimum < maximum) { + this.update(minimum, maximum - minimum); + } + } + /** + * @param {number} offset + * @param {number} limit + */ + async update(offset, limit) { + this.state.isDisabled = true; + await this.props.onUpdate({ offset, limit }); + this.state.isDisabled = false; + this.state.isEditing = false; + } + + /** + * @param {MouseEvent} ev + */ + onClickAway(ev) { + if (ev.target !== this.inputRef.el) { + this.state.isEditing = false; + } + } + onInputBlur() { + this.state.isEditing = false; + } + /** + * @param {Event} ev + */ + onInputChange(ev) { + this.setValue(ev.target.value); + if (!this.state.isDisabled) { + ev.preventDefault(); + } + } + /** + * @param {KeyboardEvent} ev + */ + onInputKeydown(ev) { + switch (ev.key) { + case "Enter": + ev.preventDefault(); + ev.stopPropagation(); + this.setValue(ev.currentTarget.value); + break; + case "Escape": + ev.preventDefault(); + ev.stopPropagation(); + this.state.isEditing = false; + break; + } + } + onValueClick() { + if (this.props.isEditable && !this.state.isEditing && !this.state.isDisabled) { + this.forceFocus(); + this.state.isEditing = true; + } + } +} +Pager.template = "web.Pager"; + +Pager.defaultProps = { + isEditable: true, + withAccessKey: true, +}; +Pager.props = { + offset: Number, + limit: Number, + total: Number, + onUpdate: Function, + isEditable: Boolean, + withAccessKey: Boolean, +}; diff --git a/addons/web/static/src/core/pager/pager.scss b/addons/web/static/src/core/pager/pager.scss new file mode 100644 index 00000000000..962e6a2bbc6 --- /dev/null +++ b/addons/web/static/src/core/pager/pager.scss @@ -0,0 +1,24 @@ +.o_pager { + display: flex; + align-items: center; + + .o_pager_counter { + min-width: $o-statbutton-height; + } + + .o_pager_value { + display: inline-block; + } + + span.o_pager_value { + border-bottom: 1px solid transparent; + } + + input.o_pager_value { + text-align: right; + width: 60px; + &:focus { + outline: none; + } + } +} diff --git a/addons/web/static/src/core/pager/pager.xml b/addons/web/static/src/core/pager/pager.xml new file mode 100644 index 00000000000..b8233fe2386 --- /dev/null +++ b/addons/web/static/src/core/pager/pager.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/addons/web/static/src/legacy/js/components/pager.js b/addons/web/static/src/legacy/js/components/pager.js index 2d8d402afb1..92f6e3e927d 100644 --- a/addons/web/static/src/legacy/js/components/pager.js +++ b/addons/web/static/src/legacy/js/components/pager.js @@ -219,7 +219,7 @@ odoo.define('web.Pager', function (require) { validate: Function, withAccessKey: Boolean, }; - Pager.template = 'web.Pager'; + Pager.template = 'web.legacy.Pager'; return Pager; }); diff --git a/addons/web/static/src/legacy/xml/base.xml b/addons/web/static/src/legacy/xml/base.xml index 54c5353210b..5bf1da2dcbb 100644 --- a/addons/web/static/src/legacy/xml/base.xml +++ b/addons/web/static/src/legacy/xml/base.xml @@ -292,7 +292,7 @@ - +