diff --git a/src/Interactable.js b/src/Interactable.js index 8464db5e3..a775f9925 100644 --- a/src/Interactable.js +++ b/src/Interactable.js @@ -1,3 +1,4 @@ +const clone = require('./utils/clone'); const is = require('./utils/is'); const events = require('./utils/events'); const extend = require('./utils/extend'); @@ -63,8 +64,9 @@ class Interactable { if (option in defaults[action]) { // if the option in the options arg is an object value if (is.object(options[option])) { - // duplicate the object - this.options[action][option] = extend(this.options[action][option] || {}, options[option]); + // duplicate the object and merge + this.options[action][option] = clone(this.options[action][option] || {}); + extend(this.options[action][option], options[option]); if (is.object(defaults.perAction[option]) && 'enabled' in defaults.perAction[option]) { this.options[action][option].enabled = options[option].enabled === false? false : true; @@ -294,14 +296,14 @@ class Interactable { options = {}; } - this.options = extend({}, defaults.base); + this.options = clone(defaults.base); - const perActions = extend({}, defaults.perAction); + const perActions = clone(defaults.perAction); for (const actionName in actions.methodDict) { const methodName = actions.methodDict[actionName]; - this.options[actionName] = extend({}, defaults[actionName]); + this.options[actionName] = clone(defaults[actionName]); this.setPerAction(actionName, perActions); diff --git a/src/utils/clone.js b/src/utils/clone.js new file mode 100644 index 000000000..4a4c80122 --- /dev/null +++ b/src/utils/clone.js @@ -0,0 +1,13 @@ +const is = require('./is'); + +module.exports = function clone (source) { + const dest = {}; + for (const prop in source) { + if (is.object(source[prop])) { + dest[prop] = clone(source[prop]); + } else { + dest[prop] = source[prop]; + } + } + return dest; +}; diff --git a/tests/Interactable.js b/tests/Interactable.js new file mode 100644 index 000000000..7173cdee2 --- /dev/null +++ b/tests/Interactable.js @@ -0,0 +1,84 @@ +const test = require('./test'); +const d = require('./domator'); + +const Interactable = require('../src/Interactable'); +const actions = require('../src/actions/base'); + +test('Interactable copies and extends defaults', t => { + actions.methodDict.test = 'testize'; + Interactable.prototype.testize = function (options) { + this.setPerAction('test', options); + }; + + const defaults = require('../src/defaultOptions'); + defaults.test = { + fromDefault: { a: 1, b: 2 }, + specified: { c: 1, d: 2 }, + }; + + const specified = { specified: 'parent' }; + + const div = d('div'); + const interactable = new Interactable(div, { test: specified }); + + t.deepEqual(interactable.options.test.specified, specified.specified, + 'specified options are properly set'); + t.deepEqual(interactable.options.test.fromDefault, defaults.test.fromDefault, + 'default options are properly set'); + t.notEqual(interactable.options.test.fromDefault, defaults.test.fromDefault, + 'defaults are not aliased'); + + defaults.test.fromDefault.c = 3; + t.notOk('c' in interactable.options.test.fromDefault, + 'modifying defaults does not affect constructed interactables'); + + // Undo global changes + delete actions.methodDict.test; + delete Interactable.prototype.testize; + delete defaults.test; + + t.end(); +}); + +test('Interactable copies and extends per action defaults', t => { + actions.methodDict.test = 'testize'; + Interactable.prototype.testize = function (options) { + this.setPerAction('test', options); + }; + + const defaults = require('../src/defaultOptions'); + defaults.perAction.testModifier = { + fromDefault: { a: 1, b: 2 }, + specified: null, + }; + defaults.test = { testModifier: defaults.perAction.testModifier }; + + const div = d('div'); + const interactable = new Interactable(div, {}); + interactable.testize({ testModifier: { specified: 'parent' } }); + + t.deepEqual(interactable.options.test, { testModifier: { + fromDefault: { a: 1, b: 2}, + specified: 'parent', + }}, 'specified options are properly set'); + t.deepEqual( + interactable.options.test.testModifier.fromDefault, + defaults.perAction.testModifier.fromDefault, + 'default options are properly set'); + t.notEqual( + interactable.options.test.testModifier.fromDefault, + defaults.perAction.testModifier.fromDefault, + 'defaults are not aliased'); + + defaults.perAction.testModifier.fromDefault.c = 3; + t.notOk('c' in interactable.options.test.testModifier.fromDefault, + 'modifying defaults does not affect constructed interactables'); + + // Undo global changes + delete actions.methodDict.test; + delete Interactable.prototype.test; + delete defaults.test; + delete defaults.perAction.testModifier; + + t.end(); +}); diff --git a/tests/Interaction.js b/tests/Interaction.js index 9bcb59c90..66de9077e 100644 --- a/tests/Interaction.js +++ b/tests/Interaction.js @@ -253,7 +253,7 @@ test('Interaction.start', t => { const Interaction = require('../src/Interaction'); const interaction = new Interaction({}); const action = { name: 'TEST' }; - const target = {}; + const target = helpers.mockInteractable(); const element = {}; const pointer = helpers.newPointer(); const event = {}; diff --git a/tests/helpers.js b/tests/helpers.js index c6af06a3f..b692ab1e3 100644 --- a/tests/helpers.js +++ b/tests/helpers.js @@ -1,5 +1,6 @@ const _ = require('lodash'); const { window: { document } } = require('../src/utils/window'); +const utils = require('../src/utils'); let counter = 0; @@ -64,6 +65,57 @@ const helpers = { createEl (name) { return document.createElement(name); }, + + mockScope (options) { + return Object.assign({ + documents: [], + defaults: require('../src/defaultOptions'), + interactions: [], + signals: require('../src/utils/Signals').new(), + Interaction: { + signals: require('../src/utils/Signals').new(), + new () { + return {}; + }, + }, + InteractEvent: { + signals: require('../src/utils/Signals').new(), + }, + Interactable: { + signals: require('../src/utils/Signals').new(), + }, + }, options); + }, + + mockSignals () { + return { + on () {}, + off () {}, + fire () {}, + }; + }, + + mockInteractable (props) { + const Eventable = require('../src/Eventable'); + + return Object.assign( + { + options: { + deltaSource: 'page', + }, + target: {}, + events: new Eventable(), + getRect () { + return this.element + ? utils.dom.getClientRect(this.element) + : { left: 0, top: 0, right: 0, bottom: 0 }; + }, + fire (event) { + this.events.fire(event); + }, + }, + props); + }, }; module.exports = helpers; diff --git a/tests/index.js b/tests/index.js index 4d58529de..8e4cd1f90 100644 --- a/tests/index.js +++ b/tests/index.js @@ -1,3 +1,4 @@ +require('./Interactable'); require('./Interaction'); // Legacy browser support