diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/component_hooks | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/mail/static/src/component_hooks')
4 files changed, 429 insertions, 0 deletions
diff --git a/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js b/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js new file mode 100644 index 00000000..f96eb265 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js @@ -0,0 +1,93 @@ +odoo.define('mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js', function (require) { +'use strict'; + +const { useState, onMounted, onWillUnmount } = owl.hooks; + +/** + * This hook handle the visibility of the dropzone based on drag & drop events. + * It needs a ref to a dropzone, so you need to specify a t-ref="dropzone" in + * the template of your component. + * + * @returns {Object} + */ +function useDragVisibleDropZone() { + /** + * Determine whether the drop zone should be visible or not. + * Note that this is an observed value, and primitive types such as + * boolean cannot be observed, hence this is an object with boolean + * value accessible from `.value` + */ + const isVisible = useState({ value: false }); + + /** + * Counts how many drag enter/leave happened globally. This is the only + * way to know if a file has been dragged out of the browser window. + */ + let dragCount = 0; + + // COMPONENTS HOOKS + onMounted(() => { + document.addEventListener('dragenter', _onDragenterListener, true); + document.addEventListener('dragleave', _onDragleaveListener, true); + document.addEventListener('drop', _onDropListener, true); + + // Thoses Events prevent the browser to open or download the file if + // it's dropped outside of the dropzone + window.addEventListener('dragover', ev => ev.preventDefault()); + window.addEventListener('drop', ev => ev.preventDefault()); + }); + + onWillUnmount(() => { + document.removeEventListener('dragenter', _onDragenterListener, true); + document.removeEventListener('dragleave', _onDragleaveListener, true); + document.removeEventListener('drop', _onDropListener, true); + + window.removeEventListener('dragover', ev => ev.preventDefault()); + window.removeEventListener('drop', ev => ev.preventDefault()); + }); + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Shows the dropzone when entering the browser window, to let the user know + * where he can drop its file. + * Avoids changing state when entering inner dropzones. + * + * @private + * @param {DragEvent} ev + */ + function _onDragenterListener(ev) { + if (dragCount === 0) { + isVisible.value = true; + } + dragCount++; + } + + /** + * @private + * @param {DragEvent} ev + */ + function _onDragleaveListener(ev) { + dragCount--; + if (dragCount === 0) { + isVisible.value = false; + } + } + + /** + * @private + * @param {DragEvent} ev + */ + function _onDropListener(ev) { + dragCount = 0; + isVisible.value = false; + } + + return isVisible; +} + +return useDragVisibleDropZone; + +}); diff --git a/addons/mail/static/src/component_hooks/use_refs/use_refs.js b/addons/mail/static/src/component_hooks/use_refs/use_refs.js new file mode 100644 index 00000000..6c84f195 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_refs/use_refs.js @@ -0,0 +1,21 @@ +odoo.define('mail/static/src/component_hooks/use_refs/use_refs.js', function (require) { +'use strict'; + +const { Component } = owl; + +/** + * This hook provides support for dynamic-refs. + * + * @returns {function} returns object whose keys are t-ref values of active refs. + * and values are refs. + */ +function useRefs() { + const component = Component.current; + return function () { + return component.__owl__.refs || {}; + }; +} + +return useRefs; + +}); diff --git a/addons/mail/static/src/component_hooks/use_store/use_store.js b/addons/mail/static/src/component_hooks/use_store/use_store.js new file mode 100644 index 00000000..7ff5ea3a --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_store/use_store.js @@ -0,0 +1,126 @@ +odoo.define('mail/static/src/component_hooks/use_store/use_store.js', function (require) { +'use strict'; + +/** + * Similar to owl.hooks.useStore but to decide if a new render has to be done it + * compares the keys on the result, with an optional level of depth for each + * key, given as options `compareDepth`. + * + * It assumes that the result of the selector is always an object (or array). + * + * @param {function} selector function passed as selector of original `useStore` + * with 1st parameter extended as store state. @see owl.hooks.useStore + * @param {object} [options={}] @see owl.hooks.useStore + * @param {number|object} [options.compareDepth=0] the comparison depth, either + * as number (applies to all keys) or as an object (depth for specific keys) + * @returns {Proxy} @see owl.hooks.useStore + */ +function useStore(selector, options = {}) { + const store = options.store || owl.Component.current.env.store; + const hashFn = store.observer.revNumber.bind(store.observer); + const isEqual = options.isEqual || ((a, b) => a === b); + + /** + * Returns a function comparing whether two values are the same, which is just + * calling `isEqual` on primitive values and objects, but which also works for + * Owl Proxy in a temporal way: the current result of hashFn is compared to the + * previous result of hashFn (from the last time the function was called). + * + * It means that when this function is given Proxy the first call will always + * return false, and consecutive calls will not lead to the same result: + * it returns true the first time after a change happended inside the Proxy, and + * then always returns false until a new change is made. + * + * @returns {function} function taking two arguments, and comparing them as + * explained above. + */ + function proxyComparator() { + /** + * It is important to locally save the old `revNumber` of each resulting + * value because when the "old" and "new" values are the same proxy it is + * impossible to compare them based on their current value (since it was + * updated in "both" due to the fact it is a proxy in the first place). + * + * And if the values are not proxy, `revNumber` will be 0 and the `isEqual` + * will be used to compare them. + */ + let oldRevNumber; + + function compare(a, b) { + let ok = true; + const newRevNumber = hashFn(b); + if (a === b && newRevNumber > 0) { + ok = oldRevNumber === newRevNumber; + } else { + ok = isEqual(a, b); + } + oldRevNumber = newRevNumber; + return ok; + } + + return compare; + } + + /** + * @see proxyComparator, but instead of comparing the given values, it compares + * their respective keys, with `compareDepth` level of depth. + * 0 = compare key, 1 = also compare subkeys, ... + * + * This assumes the given values are objects or arrays. + * + * @param {number|object} compareDepth the comparison depth, either as number + * (applies to all keys) or as an object (depth for specific keys) + * @returns {function} + */ + function proxyComparatorDeep(compareDepth = 0) { + const comparator = proxyComparator(); + const comparators = {}; + + function compare(a, b) { + // If a and b are (the same) proxy, it is already managing the depth + // by itself, and a simple comparator can be used. + if (a === b && hashFn(b) > 0) { + return comparator(a, b); + } + let ok = true; + const newKeys = Object.keys(b); + if (!a || (Object.keys(a).length !== newKeys.length)) { + ok = false; + } + for (const key of newKeys) { + // the depth can be given either as a number (for all keys) or as + // an object (for each key) + let depth; + if (typeof compareDepth === 'number') { + depth = compareDepth; + } else { + depth = compareDepth[key] || 0; + } + if (!(key in comparators)) { + if (depth > 0) { + comparators[key] = proxyComparatorDeep(depth - 1); + } else { + comparators[key] = proxyComparator(); + } + } + // It is important to not break too early, the comparator has to + // be called for every key to remember their current states. + if (!comparators[key](a ? a[key] : a, b[key])) { + ok = false; + } + } + return ok; + } + + return compare; + } + + const extendedSelector = (state, props) => selector(props); + return owl.hooks.useStore(extendedSelector, Object.assign({}, options, { + isEqual: proxyComparatorDeep(options.compareDepth), + })); +} + +return useStore; + +}); diff --git a/addons/mail/static/src/component_hooks/use_store/use_store_tests.js b/addons/mail/static/src/component_hooks/use_store/use_store_tests.js new file mode 100644 index 00000000..f1a4b054 --- /dev/null +++ b/addons/mail/static/src/component_hooks/use_store/use_store_tests.js @@ -0,0 +1,189 @@ +odoo.define('mail/static/src/component_hooks/use_store/use_store_tests.js', function (require) { +'use strict'; + +const useStore = require('mail/static/src/component_hooks/use_store/use_store.js'); +const { + afterNextRender, + nextAnimationFrame, +} = require('mail/static/src/utils/test_utils.js'); + +const { Component, QWeb, Store } = owl; +const { onPatched, useGetters } = owl.hooks; +const { xml } = owl.tags; + +QUnit.module('mail', {}, function () { +QUnit.module('component_hooks', {}, function () { +QUnit.module('use_store', {}, function () { +QUnit.module('use_store_tests.js', { + beforeEach() { + const qweb = new QWeb(); + this.env = { qweb }; + }, + afterEach() { + this.env = undefined; + this.store = undefined; + }, +}); + + +QUnit.test("compare keys, no depth, primitives", async function (assert) { + assert.expect(8); + this.store = new Store({ + env: this.env, + getters: { + get({ state }, key) { + return state[key]; + }, + }, + state: { + obj: { + subObj1: 'a', + subObj2: 'b', + use1: true, + }, + }, + }); + this.env.store = this.store; + let count = 0; + class MyComponent extends Component { + constructor() { + super(...arguments); + this.storeGetters = useGetters(); + this.storeProps = useStore(props => { + const obj = this.storeGetters.get('obj'); + return { + res: obj.use1 ? obj.subObj1 : obj.subObj2, + }; + }); + onPatched(() => { + count++; + }); + } + } + Object.assign(MyComponent, { + env: this.env, + props: {}, + template: xml`<div t-esc="storeProps.res"/>`, + }); + + const fixture = document.querySelector('#qunit-fixture'); + + const myComponent = new MyComponent(); + await myComponent.mount(fixture); + assert.strictEqual(count, 0, + 'should not detect an update initially'); + assert.strictEqual(fixture.textContent, 'a', + 'should display the content of subObj1'); + + await afterNextRender(() => { + this.store.state.obj.use1 = false; + }); + assert.strictEqual(count, 1, + 'should detect an update because the selector is returning a different value (was subObj1, now is subObj2)'); + assert.strictEqual(fixture.textContent, 'b', + 'should display the content of subObj2'); + + this.store.state.obj.subObj2 = 'b'; + // there must be no render here + await nextAnimationFrame(); + assert.strictEqual(count, 1, + 'should not detect an update because the same primitive value was assigned (subObj2 was already "b")'); + assert.strictEqual(fixture.textContent, 'b', + 'should still display the content of subObj2'); + + await afterNextRender(() => { + this.store.state.obj.subObj2 = 'd'; + }); + assert.strictEqual(count, 2, + 'should detect an update because the selector is returning a different value for subObj2'); + assert.strictEqual(fixture.textContent, 'd', + 'should display the new content of subObj2'); + + myComponent.destroy(); +}); + +QUnit.test("compare keys, depth 1, proxy", async function (assert) { + assert.expect(8); + this.store = new Store({ + env: this.env, + getters: { + get({ state }, key) { + return state[key]; + }, + }, + state: { + obj: { + subObj1: { a: 'a' }, + subObj2: { a: 'b' }, + use1: true, + }, + }, + }); + this.env.store = this.store; + let count = 0; + class MyComponent extends Component { + constructor() { + super(...arguments); + this.storeGetters = useGetters(); + this.storeProps = useStore(props => { + const obj = this.storeGetters.get('obj'); + return { + array: [obj.use1 ? obj.subObj1 : obj.subObj2], + }; + }, { + compareDepth: { + array: 1, + }, + }); + onPatched(() => { + count++; + }); + } + } + Object.assign(MyComponent, { + env: this.env, + props: {}, + template: xml`<div t-esc="storeProps.array[0].a"/>`, + }); + + const fixture = document.querySelector('#qunit-fixture'); + + const myComponent = new MyComponent(); + await myComponent.mount(fixture); + assert.strictEqual(count, 0, + 'should not detect an update initially'); + assert.strictEqual(fixture.textContent, 'a', + 'should display the content of subObj1'); + + await afterNextRender(() => { + this.store.state.obj.use1 = false; + }); + assert.strictEqual(count, 1, + 'should detect an update because the selector is returning a different value (was subObj1, now is subObj2)'); + assert.strictEqual(fixture.textContent, 'b', + 'should display the content of subObj2'); + + this.store.state.obj.subObj1.a = 'c'; + // there must be no render here + await nextAnimationFrame(); + assert.strictEqual(count, 1, + 'should not detect an update because subObj1 was changed but only subObj2 is selected'); + assert.strictEqual(fixture.textContent, 'b', + 'should still display the content of subObj2'); + + await afterNextRender(() => { + this.store.state.obj.subObj2.a = 'd'; + }); + assert.strictEqual(count, 2, + 'should detect an update because the value of subObj2 changed'); + assert.strictEqual(fixture.textContent, 'd', + 'should display the new content of subObj2'); + + myComponent.destroy(); +}); + +}); +}); +}); + +}); |
