summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/core/custom_hooks.js
blob: 883f385ac3c0cbd7d9e54182306c40e2dfc671d8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
odoo.define('web.custom_hooks', function () {
    "use strict";

    const { Component, hooks } = owl;
    const { onMounted, onPatched, onWillUnmount } = hooks;

    /**
     * Focus a given selector as soon as it appears in the DOM and if it was not
     * displayed before. If the selected target is an input|textarea, set the selection
     * at the end.
     * @param {Object} [params]
     * @param {string} [params.selector='autofocus'] default: select the first element
     *                 with an `autofocus` attribute.
     * @returns {Function} function that forces the focus on the next update if visible.
     */
    function useAutofocus(params = {}) {
        const comp = Component.current;
        // Prevent autofocus in mobile
        if (comp.env.device.isMobile) {
            return () => {};
        }
        const selector = params.selector || '[autofocus]';
        let target = null;
        function autofocus() {
            const prevTarget = target;
            target = comp.el.querySelector(selector);
            if (target && target !== prevTarget) {
                target.focus();
                if (['INPUT', 'TEXTAREA'].includes(target.tagName)) {
                    target.selectionStart = target.selectionEnd = target.value.length;
                }
            }
        }
        onMounted(autofocus);
        onPatched(autofocus);

        return function focusOnUpdate() {
            target = null;
        };
    }

    /**
     * The useListener hook offers an alternative to Owl's classical event
     * registration mechanism (with attribute 't-on-eventName' in xml). It is
     * especially useful for abstract components, meant to be extended by
     * specific ones. If those abstract components need to define event handlers,
     * but don't have any template (because the template completely depends on
     * specific cases), then using the 't-on' mechanism isn't adequate, as the
     * handlers would be lost by the template override. In this case, using this
     * hook instead is more convenient.
     *
     * Example: navigation event handling in AbstractField
     *
     * Usage: like all Owl hooks, this function has to be called in the
     * constructor of an Owl component:
     *
     *   useListener('click', () => { console.log('clicked'); });
     *
     * An optional native query selector can be specified as second argument for
     * event delegation. In this case, the handler is only called if the event
     * is triggered on an element matching the given selector.
     *
     *   useListener('click', 'button', () => { console.log('clicked'); });
     *
     * Note: components that alter the event's target (e.g. Portal) are not
     * expected to behave as expected with event delegation.
     *
     * @param {string} eventName the name of the event
     * @param {string} [querySelector] a JS native selector for event delegation
     * @param {function} handler the event handler (will be bound to the component)
     * @param {Object} [addEventListenerOptions] to be passed to addEventListener as options.
     *    Useful for listening in the capture phase
     */
    function useListener(eventName, querySelector, handler, addEventListenerOptions) {
        if (typeof arguments[1] !== 'string') {
            querySelector = null;
            handler = arguments[1];
            addEventListenerOptions = arguments[2];
        }
        if (typeof handler !== 'function') {
            throw new Error('The handler must be a function');
        }

        const comp = Component.current;
        let boundHandler;
        if (querySelector) {
            boundHandler = function (ev) {
                let el = ev.target;
                let target;
                while (el && !target) {
                    if (el.matches(querySelector)) {
                        target = el;
                    } else if (el === comp.el) {
                        el = null;
                    } else {
                        el = el.parentElement;
                    }
                }
                if (el) {
                    handler.call(comp, ev);
                }
            };
        } else {
            boundHandler = handler.bind(comp);
        }
        onMounted(function () {
            comp.el.addEventListener(eventName, boundHandler, addEventListenerOptions);
        });
        onWillUnmount(function () {
            comp.el.removeEventListener(eventName, boundHandler, addEventListenerOptions);
        });
    }

    return {
        useAutofocus,
        useListener,
    };
});