summaryrefslogtreecommitdiff
path: root/addons/mail/static/src/component_hooks
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/mail/static/src/component_hooks
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff)
initial commit 2
Diffstat (limited to 'addons/mail/static/src/component_hooks')
-rw-r--r--addons/mail/static/src/component_hooks/use_drag_visible_dropzone/use_drag_visible_dropzone.js93
-rw-r--r--addons/mail/static/src/component_hooks/use_refs/use_refs.js21
-rw-r--r--addons/mail/static/src/component_hooks/use_store/use_store.js126
-rw-r--r--addons/mail/static/src/component_hooks/use_store/use_store_tests.js189
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();
+});
+
+});
+});
+});
+
+});