diff --git a/src/helpers/helpers.config.js b/src/helpers/helpers.config.js index 65c415b80b1..a785fa39a5a 100644 --- a/src/helpers/helpers.config.js +++ b/src/helpers/helpers.config.js @@ -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) { diff --git a/test/specs/helpers.config.tests.js b/test/specs/helpers.config.tests.js index 2075af5e973..b79c0b9b822 100644 --- a/test/specs/helpers.config.tests.js +++ b/test/specs/helpers.config.tests.js @@ -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() {