diff --git a/src/autoStart/base.js b/src/autoStart/base.js index f3f3d1b66..d90486ae4 100644 --- a/src/autoStart/base.js +++ b/src/autoStart/base.js @@ -96,6 +96,7 @@ function init (scope) { scope.autoStart = { // Allow this many interactions to happen simultaneously maxInteractions: Infinity, + withinInteractionLimit, signals: new Signals(), }; } diff --git a/src/index.js b/src/index.js index dc6765a36..de394cf96 100644 --- a/src/index.js +++ b/src/index.js @@ -32,6 +32,7 @@ import restrictEdges from './modifiers/restrictEdges'; import restrictSize from './modifiers/restrictSize'; import autoScroll from './autoScroll'; +import reflow from './reflow'; export function init (window) { scopeInit(window); @@ -74,5 +75,8 @@ export function init (window) { // autoScroll interact.use(autoScroll); + // reflow + interact.use(reflow); + return interact; } diff --git a/src/reflow.js b/src/reflow.js new file mode 100644 index 000000000..312f09cd5 --- /dev/null +++ b/src/reflow.js @@ -0,0 +1,103 @@ +import interactions from './interactions'; +import { + arr, + is, + extend, + rect as rectUtils, + pointer as pointerUtils, +} from './utils'; + +export function init (scope) { + const { + actions, + Interaction, + /** @lends Interactable */ + Interactable, + } = scope; + + // add action reflow event types + for (const actionName of actions.names) { + actions.eventTypes.push(`${actionName}reflow`); + } + + // remove completed reflow interactions + Interaction.signals.on('stop', ({ interaction }) => { + if (interaction.pointerType === 'reflow') { + arr.remove(scope.interactions, interaction); + } + }); + + /** + * ```js + * const interactable = interact(target); + * const drag = { name: drag, axis: 'x' }; + * const resize = { name: resize, edges: { left: true, bottom: true }; + * + * interactable.reflow(drag); + * interactable.reflow(resize); + * ``` + * + * Start an action sequence to re-apply modifiers, check drops, etc. + * + * @param { Object } action The action to begin + * @param { string } action.name The name of the action + */ + Interactable.prototype.reflow = function (action) { + return reflow(this, action, scope); + }; +} + +function reflow (interactable, action, scope) { + let elements = is.string(interactable.target) + ? arr.from(interactable._context.querySelectorAll(interactable.target)) + : [interactable.target]; + + // follow autoStart max interaction settings + if (scope.autoStart) { + elements = elements.filter( + element => scope.autoStart.withinInteractionLimit(interactable, element, action, scope)); + } + + for (const element of elements) { + const interaction = interactions.newInteraction({ pointerType: 'reflow' }, scope); + + const rect = interactable.getRect(element); + + if (!rect) { break; } + + const xywh = rectUtils.tlbrToXywh(rect); + const coords = { + page: xywh, + client: xywh, + }; + const event = extend(pointerUtils.coordsToEvent(coords), coords); + const signalArg = { + interaction, + event, + pointer: event, + eventTarget: element, + phase: 'reflow', + }; + + interaction.target = interactable; + interaction.element = element; + interaction.prepared = extend({}, action); + interaction.prevEvent = event; + interaction.updatePointer(event, event, element, true); + + interaction._doPhase(signalArg); + + signalArg.phase = 'start'; + interaction._interacting = interaction._doPhase(signalArg); + + if (interaction._interacting) { + interaction.move(signalArg); + interaction.end(event); + } + else { + interaction.stop(); + } + } +} + +export default { init }; diff --git a/tests/index.js b/tests/index.js index dc82058da..278dd196e 100644 --- a/tests/index.js +++ b/tests/index.js @@ -43,6 +43,8 @@ import './actions/drag'; // autoScroll //import './autoScroll'; +import './reflow'; + import './interact'; //const index = import '../src/index'; diff --git a/tests/reflow.js b/tests/reflow.js new file mode 100644 index 000000000..c43382598 --- /dev/null +++ b/tests/reflow.js @@ -0,0 +1,52 @@ +import test from './test'; +import * as helpers from './helpers'; +import reflow from '../src/reflow'; +import win from '../src/utils/window'; + +test('reflow', t => { + const scope = helpers.mockScope({ + autoStart: {}, + }); + + Object.assign(scope.actions, { test: {}, names: ['test'] }); + + reflow.init(scope); + + t.ok( + scope.Interactable.prototype.reflow instanceof Function, + 'reflow method is added to Interactable.prototype' + ); + + const fired = []; + const interactable = helpers.newInteractable(scope, win.window); + const rect = Object.freeze({ top: 100, left: 200, bottom: 300, right: 400 }); + interactable.fire = iEvent => fired.push(iEvent); + interactable.target = {}; + interactable.options.test = {}; + interactable.rectChecker(() => rect); + + scope.autoStart.withinInteractionLimit = () => false; + t.equal(fired.length, 0, 'follows scope.autoStart.withinInteractionLimit'); + + scope.autoStart.withinInteractionLimit = () => true; + interactable.reflow({ name: 'test' }); + + const phases = ['reflow', 'start', 'move', 'end']; + + for (const [index, phase] of Object.entries(phases)) { + t.equal(fired[index].type, `test${phase}`, `event #${index} is ${phase}`); + } + + const interaction = fired[0].interaction; + + t.deepEqual( + interaction.startCoords.page, + { + x: rect.left, + y: rect.top, + }, + 'uses element top left for event coords' + ); + + t.end(); +});