]> git.ipfire.org Git - thirdparty/Chart.js.git/commitdiff
Update resolver fallback logic (#8456)
authorJukka Kurkela <jukka.kurkela@gmail.com>
Fri, 19 Feb 2021 06:30:39 +0000 (08:30 +0200)
committerGitHub <noreply@github.com>
Fri, 19 Feb 2021 06:30:39 +0000 (08:30 +0200)
src/helpers/helpers.config.js
test/specs/helpers.config.tests.js

index 65c415b80b15bc3bc72933300b7b0fc4ac110060..a785fa39a5abe31c552b664f170ad3db0a734a8f 100644 (file)
@@ -1,4 +1,4 @@
-import {defined, isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core';
+import {defined, isArray, isFunction, isObject, resolveObjectKey, valueOrDefault, _capitalize} from './helpers.core';
 
 /**
  * Creates a Proxy for resolving raw values for options.
@@ -12,7 +12,7 @@ export function _createResolver(scopes, prefixes = ['']) {
     [Symbol.toStringTag]: 'Object',
     _cacheable: true,
     _scopes: scopes,
-    override: (scope) => _createResolver([scope].concat(scopes), prefixes),
+    override: (scope) => _createResolver([scope, ...scopes], prefixes),
   };
   return new Proxy(cache, {
     /**
@@ -186,7 +186,7 @@ function _resolveScriptable(prop, value, target, receiver) {
   _stack.delete(prop);
   if (isObject(value)) {
     // When scriptable option returns an object, create a resolver on that.
-    value = createSubResolver([value].concat(_proxy._scopes), prop, value);
+    value = createSubResolver(_proxy._scopes, prop, value);
   }
   return value;
 }
@@ -202,7 +202,7 @@ function _resolveArray(prop, value, target, isIndexable) {
     const scopes = _proxy._scopes.filter(s => s !== arr);
     value = [];
     for (const item of arr) {
-      const resolver = createSubResolver([item].concat(scopes), prop, item);
+      const resolver = createSubResolver(scopes, prop, item);
       value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop]));
     }
   }
@@ -211,12 +211,20 @@ function _resolveArray(prop, value, target, isIndexable) {
 
 function createSubResolver(parentScopes, prop, value) {
   const set = new Set([value]);
-  const {keys, includeParents} = _resolveSubKeys(parentScopes, prop, value);
-  for (const key of keys) {
-    for (const item of parentScopes) {
+  const lookupScopes = [value, ...parentScopes];
+  const {keys, includeParents} = _resolveSubKeys(lookupScopes, prop, value);
+  while (keys.length) {
+    const key = keys.shift();
+    for (const item of lookupScopes) {
       const scope = resolveObjectKey(item, key);
       if (scope) {
         set.add(scope);
+        // fallback detour?
+        const fallback = scope._fallback;
+        if (defined(fallback)) {
+          keys.push(...resolveFallback(fallback, key, scope).filter(k => k !== key));
+        }
+
       } else if (key !== prop && scope === false) {
         // If any of the fallback scopes is explicitly false, return false
         // For example, options.hover falls back to options.interaction, when
@@ -231,14 +239,18 @@ function createSubResolver(parentScopes, prop, value) {
   return _createResolver([...set]);
 }
 
+function resolveFallback(fallback, prop, value) {
+  const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
+  return isArray(resolved) ? resolved : typeof resolved === 'string' ? [resolved] : [];
+}
+
 function _resolveSubKeys(parentScopes, prop, value) {
-  const fallback = _resolve('_fallback', parentScopes.map(scope => scope[prop] || scope));
+  const fallback = valueOrDefault(_resolve('_fallback', parentScopes.map(scope => scope[prop] || scope)), true);
   const keys = [prop];
   if (defined(fallback)) {
-    const resolved = isFunction(fallback) ? fallback(prop, value) : fallback;
-    keys.push(...(isArray(resolved) ? resolved : [resolved]));
+    keys.push(...resolveFallback(fallback, prop, value));
   }
-  return {keys: keys.filter(v => v), includeParents: fallback !== prop};
+  return {keys: keys.filter(v => v), includeParents: fallback !== false && fallback !== prop};
 }
 
 function _resolveWithPrefixes(prop, prefixes, scopes) {
index 2075af5e973d3096d3a6117f5d709309bff7a340..b79c0b9b82265add1f148762ec2a44220d647104 100644 (file)
@@ -35,34 +35,6 @@ describe('Chart.helpers.config', function() {
       expect(sub.opt).toEqual('opt');
     });
 
-    it('should follow _fallback', function() {
-      const defaults = {
-        interaction: {
-          mode: 'test',
-          priority: 'fall'
-        },
-        hover: {
-          _fallback: 'interaction',
-          priority: 'main'
-        }
-      };
-      const options = {
-        interaction: {
-          a: 1
-        },
-        hover: {
-          b: 2
-        }
-      };
-      const resolver = _createResolver([options, defaults]);
-      expect(resolver.hover).toEqualOptions({
-        mode: 'test',
-        priority: 'main',
-        a: 1,
-        b: 2
-      });
-    });
-
     it('should support overriding options', function() {
       const defaults = {
         option1: 'defaults1',
@@ -123,6 +95,229 @@ describe('Chart.helpers.config', function() {
 
       expect(resolver.getter).toEqual('options getter');
     });
+
+    describe('_fallback', function() {
+      it('should follow simple _fallback', function() {
+        const defaults = {
+          interaction: {
+            mode: 'test',
+            priority: 'fall'
+          },
+          hover: {
+            _fallback: 'interaction',
+            priority: 'main'
+          }
+        };
+        const options = {
+          interaction: {
+            a: 1
+          },
+          hover: {
+            b: 2
+          }
+        };
+        const resolver = _createResolver([options, defaults]);
+        expect(resolver.hover).toEqualOptions({
+          mode: 'test',
+          priority: 'main',
+          a: 1,
+          b: 2
+        });
+      });
+
+      it('should not fallback when _fallback is false', function() {
+        const defaults = {
+          hover: {
+            _fallback: false,
+            a: 'defaults.hover'
+          },
+          controllers: {
+            y: 'defaults.controllers',
+            bar: {
+              z: 'defaults.controllers.bar',
+              hover: {
+                b: 'defaults.controllers.bar.hover'
+              }
+            }
+          },
+          x: 'defaults root'
+        };
+        const options = {
+          x: 'options',
+          hover: {
+            c: 'options.hover',
+            sub: {
+              f: 'options.hover.sub'
+            }
+          },
+          controllers: {
+            y: 'options.controllers',
+            bar: {
+              z: 'options.controllers.bar',
+              hover: {
+                d: 'options.controllers.bar.hover',
+                sub: {
+                  e: 'options.controllers.bar.hover.sub'
+                }
+              }
+            }
+          }
+        };
+        const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
+        expect(resolver.hover).toEqualOptions({
+          a: 'defaults.hover',
+          b: 'defaults.controllers.bar.hover',
+          c: 'options.hover',
+          d: 'options.controllers.bar.hover',
+          e: undefined,
+          f: undefined,
+          x: undefined,
+          y: undefined,
+          z: undefined
+        });
+        expect(resolver.hover.sub).toEqualOptions({
+          a: undefined,
+          b: undefined,
+          c: undefined,
+          d: undefined,
+          e: 'options.controllers.bar.hover.sub',
+          f: 'options.hover.sub',
+          x: undefined,
+          y: undefined,
+          z: undefined
+        });
+      });
+
+      it('should fallback to specific scope', function() {
+        const defaults = {
+          hover: {
+            _fallback: 'hover',
+            a: 'defaults.hover'
+          },
+          controllers: {
+            y: 'defaults.controllers',
+            bar: {
+              z: 'defaults.controllers.bar',
+              hover: {
+                b: 'defaults.controllers.bar.hover'
+              }
+            }
+          },
+          x: 'defaults root'
+        };
+        const options = {
+          x: 'options',
+          hover: {
+            c: 'options.hover',
+            sub: {
+              f: 'options.hover.sub'
+            }
+          },
+          controllers: {
+            y: 'options.controllers',
+            bar: {
+              z: 'options.controllers.bar',
+              hover: {
+                d: 'options.controllers.bar.hover',
+                sub: {
+                  e: 'options.controllers.bar.hover.sub'
+                }
+              }
+            }
+          }
+        };
+        const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]);
+        expect(resolver.hover).toEqualOptions({
+          a: 'defaults.hover',
+          b: 'defaults.controllers.bar.hover',
+          c: 'options.hover',
+          d: 'options.controllers.bar.hover',
+          e: undefined,
+          f: undefined,
+          x: undefined,
+          y: undefined,
+          z: undefined
+        });
+        expect(resolver.hover.sub).toEqualOptions({
+          a: 'defaults.hover',
+          b: 'defaults.controllers.bar.hover',
+          c: 'options.hover',
+          d: 'options.controllers.bar.hover',
+          e: 'options.controllers.bar.hover.sub',
+          f: 'options.hover.sub',
+          x: undefined,
+          y: undefined,
+          z: undefined
+        });
+      });
+
+      it('should fallback throuhg multiple routes', function() {
+        const defaults = {
+          root: {
+            a: 'root'
+          },
+          level1: {
+            _fallback: 'root',
+            b: 'level1',
+          },
+          level2: {
+            _fallback: 'level1',
+            level1: {
+              g: 'level2.level1'
+            },
+            c: 'level2',
+            sublevel1: {
+              d: 'sublevel1'
+            },
+            sublevel2: {
+              e: 'sublevel2',
+              level1: {
+                f: 'sublevel2.level1'
+              }
+            }
+          }
+        };
+        const resolver = _createResolver([defaults]);
+        expect(resolver.level1).toEqualOptions({
+          a: 'root',
+          b: 'level1',
+          c: undefined
+        });
+        expect(resolver.level2).toEqualOptions({
+          a: 'root',
+          b: 'level1',
+          c: 'level2',
+          d: undefined
+        });
+        expect(resolver.level2.sublevel1).toEqualOptions({
+          a: 'root',
+          b: 'level1',
+          c: 'level2', // TODO: this should be undefined
+          d: 'sublevel1',
+          e: undefined,
+          f: undefined,
+          g: 'level2.level1'
+        });
+        expect(resolver.level2.sublevel2).toEqualOptions({
+          a: 'root',
+          b: 'level1',
+          c: 'level2', // TODO: this should be undefined
+          d: undefined,
+          e: 'sublevel2',
+          f: undefined,
+          g: 'level2.level1'
+        });
+        expect(resolver.level2.sublevel2.level1).toEqualOptions({
+          a: 'root',
+          b: 'level1',
+          c: 'level2', // TODO: this should be undefined
+          d: undefined,
+          e: 'sublevel2', // TODO: this should be undefined
+          f: 'sublevel2.level1',
+          g: 'level2.level1'
+        });
+      });
+    });
   });
 
   describe('_attachContext', function() {