Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduction to moveable mixin #209

Merged
merged 9 commits into from
Jun 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/js/modules/components/moveable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
export const MOVE_END = 'moveend';
const PREVENT_SELECT_CLASS = 'noselect';

export function moveableComponent({ view }) {
const offset = {};
const position = {};

function onMouseMove({ clientX, clientY }) {
position.x = clientX - offset.x;
position.y = clientY - offset.y;
view.el.style.transform =
`translate3d(${position.x}px, ${position.y}px, 0)`;
}

function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp, false);
view.trigger(MOVE_END, position);
}

function onMouseDown({ clientX, clientY }) {
const { left, top } = view.el.getBoundingClientRect();
offset.x = clientX - left;
offset.y = clientY - top;
document.addEventListener('mousemove', onMouseMove, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we are binding these events to document instead to this.el? Previous version have listen on el.

Copy link
Contributor Author

@cmwd cmwd Jun 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because they worked improperly in case of objects collision. E.g when you dragged object over another and the second object has bigger z-index the first one has stopped - because we lost mouse event. Having global event solves this problem, also this way we are sure that object will never move whenever user releases mouse button.

document.addEventListener('mouseup', onMouseUp, false);
}

view.delegate('mousedown', null, onMouseDown);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not delegateEvents as described int Backbone docs? http://backbonejs.org/#View-delegateEvents

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's delegateEvetns does internally is remove all the events and assign new ones using delegate method. So by using delegate directly we saved some work and code. All events registered via delegate has unique id (view cid), which is used lately to destroy listeners. So we don't need to take care about cleaning.

view.el.classList.add(PREVENT_SELECT_CLASS);
}
36 changes: 11 additions & 25 deletions src/js/modules/plant/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ import lodash from 'lodash';
import { View } from '../../core';
import PlantViewTools from '../plant/tools';
import Const from '../../const';
import { moveableComponent, MOVE_END } from '../components/moveable';

export default View.extend({
className: 'plantingjs-plantedobject-container ui-draggable ui-draggable-handle',
className: 'plantingjs-plantedobject-container',
template: require('./object.hbs'),

events: {
'dragstart': 'dragstart',
'dragstop': 'saveCoords',
'mouseover': 'setUserActivity',
'mouseleave': 'unsetUserActivity',
},

$img: null,

initialize: function(options) {
Expand All @@ -35,24 +32,28 @@ export default View.extend({
options: this.app.data.options,
});

this.$el.draggable({
cancel: '.icon-loop, .icon-trash, .icon-resize',
});
this.model
.on('change:currentProjection', this.updateProjection, this)
.on('change:layerIndex', this.setLayer, this);

if (this.app.getState() !== Const.State.VIEWER) {
moveableComponent({ view: this });
this.on(MOVE_END, this.model.set, this.model);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually thinking here about movint this listener definition into mixin module. We have access to this there and after that there will be no reason to export MOVE_END at all.

}
},

render: function() {
const x = this.overlay.width() * this.model.get('x');
const y = this.overlay.height() / 2 + this.model.get('y') * this.overlay.width();

this.$el
.html(this.template({
projectionUrl: this.model.getProjection(),
}))
.attr('data-cid', this.model.cid)
.css({
left: this.overlay.width() * this.model.get('x'),
top: this.overlay.height() / 2 + this.model.get('y') * this.overlay.width(),
zIndex: this.model.get('layerIndex'),
transform: `translate3d(${x}px, ${y}px, 0)`,
});

this.$img = this.$el.children('img');
Expand All @@ -74,26 +75,11 @@ export default View.extend({
this.$img.attr('src', model.getProjection());
},

saveCoords: function(ev, ui) {
this.model.set({
x: ui.position.left,
y: ui.position.top,
});

return this;
},

setUserActivity: function() {
this.model.set('userActivity', true);
},

unsetUserActivity: function() {
this.model.set('userActivity', false);
},

dragstart: function(ev) {
if (this.app.getState() === Const.State.VIEWER) {
ev.preventDefault();
}
},
});
56 changes: 56 additions & 0 deletions test/component-movable-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/* global describe,beforeEach,afterEach,before,it:false */
import environment from './env/client';
import { createMouseEvent } from './utils/events';
import { moveableComponent } from '../src/js/modules/components/moveable';
import Backbone from 'backbone';
import sinon from 'sinon';
import { equal } from 'assert';

describe('Moveable component', () => {
let viewInstance;
let document;

function preparation() {
viewInstance = new Backbone.View();
viewInstance.el.getBoundingClientRect = () => ({
left: 50,
top: 50,
});
}

function cleanup() {
viewInstance.remove();
}

before(cb => environment.then((w) => {
document = w.document;
cb();
}));
beforeEach(preparation);
afterEach(cleanup);

it('Should listen to mousedown event', () => {
sinon.spy(viewInstance, 'delegate');
moveableComponent({ view: viewInstance });
equal(
viewInstance.delegate
.calledWith('mousedown', null, sinon.match.func), true);
});

it('Should move element after sequence of mousedown and mousemove events',
() => {
const spy = sinon.spy();

moveableComponent({ view: viewInstance });
viewInstance.on('moveend', spy);
equal(spy.called, false);
viewInstance.el.dispatchEvent(createMouseEvent({ type: 'mousedown' }));
document.dispatchEvent(createMouseEvent({
type: 'mousemove',
clientX: 100,
clientY: 100,
}));
document.dispatchEvent(createMouseEvent({ type: 'mouseup' }));
equal(spy.calledWithExactly({ x: 150, y: 150 }), true);
});
});
30 changes: 30 additions & 0 deletions test/utils/events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const required = param => {
throw new Error(`Parametr ${param} is required`);
};

export function createMouseEvent({
type = required('type'),
canBubble = false,
cancelable = true,
view = global.window,
detail = 0,
screenX = 0,
screenY = 0,
clientX = 0,
clientY = 0,
ctrlKey = false,
altKey = false,
shiftKey = false,
metaKey = false,
button = false,
relatedTarget = null,
}) {
const event = document.createEvent('MouseEvent');

event.initMouseEvent(
type, canBubble, cancelable, view, detail,
screenX, screenY, clientX, clientY, ctrlKey,
altKey, shiftKey, metaKey, button, relatedTarget);

return event;
}