Skip to content

Instantly share code, notes, and snippets.

@chancancode
Last active August 29, 2015 14:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save chancancode/c6af935695bf1acb8b32 to your computer and use it in GitHub Desktop.
Save chancancode/c6af935695bf1acb8b32 to your computer and use it in GitHub Desktop.
//= require jquery.ui.sortable
App.GbSortableComponent = Ember.Component.extend({
name: null,
items: '> *',
connectWith: false,
classNameBindings: [':gb-sortable', 'name'],
_didInitialize: false,
_setup: function(){
var options = this.getProperties('items', 'connectWith');
options.start = this._onSortableStart.bind(this);
options.beforeStop = this._onSortableBeforeStop.bind(this);
options.update = this._onSortableUpdate.bind(this);
options.helper = 'clone';
options.appendTo = document.body;
var $el = this.$().sortable(options);
$el.on('gbEditBegin', function(e){
$el.sortable('disable');
});
$el.on('gbEditEnd', function(e){
$el.sortable('enable');
});
this.set('_didInitialize', true);
}.on('didInsertElement'),
_teardown: function(){
this.$()
.sortable('destroy')
.off('gbEditBegin')
.off('gbEditEnd');
this.set('_didInitialize', false);
}.on('willDestroyElement'),
_itemsChanged: function(){
if(this.get('_didInitialize')){
this.$().sortable('option', 'items', this.get('items'));
}
}.observes('items'),
_connectWithChanged: function(){
if(this.get('_didInitialize')){
this.$().sortable('option', 'connectWith', this.get('connectWith'));
}
}.observes('connectWith'),
_offsetRelativeToWindow: function(e){
var offset = $(e).offset(), $window = $(window);
offset.left -= $window.scrollLeft();
offset.top -= $window.scrollTop();
return offset;
},
// Preserve the scroll position while applying any layout changes, so that an
// element still remains in the same relative position on the screen before/
// after the change
_preserveElementPosition: function(e, ui, moveHelper, block){
var $e = $(e.originalEvent.target), beforeAbs, beforeRel,
afterAbs, afterRel, dxAbs, dyAbs, dxRel, dyRel;
beforeAbs = $e.offset();
beforeRel = this._offsetRelativeToWindow($e);
block.call(this);
afterAbs = $e.offset();
afterRel = this._offsetRelativeToWindow($e);
dxAbs = Math.min(beforeAbs.left - afterAbs.left, window.pageXOffset);
dyAbs = Math.min(beforeAbs.top - afterAbs.top, window.pageYOffset);
dxRel = Math.min(beforeRel.left - afterRel.left, window.pageXOffset);
dyRel = Math.min(beforeRel.top - afterRel.top, window.pageYOffset);
window.scrollBy(-dxRel, -dyRel);
if(moveHelper){
Ember.run.later(ui.helper, 'trigger', $.Event('mousemove', {
pageX: e.originalEvent.pageX - dxAbs,
pageY: e.originalEvent.pageY - dyAbs
}), 0);
}
},
_onSortableStart: function(e, ui){
if(ui.item.hasClass('ember-view')){
var itemView = Ember.View.views[ui.item.attr('id')];
ui.item.data('gb-sortable-data', itemView.get('gbSortableData'));
}
this._preserveElementPosition(e, ui, true, function(){
this.$().addClass('sorting');
});
this.$().sortable('refreshPositions');
},
_onSortableBeforeStop: function(e, ui){
this._preserveElementPosition(e, ui, false, function(){
this.$().removeClass('sorting');
});
this.$().sortable('refreshPositions');
},
_onSortableUpdate: function(e, ui){
var data = ui.item.data('gb-sortable-data'),
position = this.$(this.get('items')).index(ui.item),
component = this;
if(position < 0){
Ember.run(component, 'sendAction', 'itemRemoved', data);
}else{
// Revert the DOM changes and let Ember re-render
// things as necessary based on the in-memory array
var originator = ui.sender || this.$();
originator.sortable('cancel');
if(ui.item.hasClass('ember-view')){
// Revmove the data to prevent memory leaks
ui.item.data('gb-sortable-data', null);
}
Ember.run(component, 'sendAction', 'itemMoved', data, position);
}
}
});
var DragMeComponent = Ember.Component.extend({
classNameBindings: [':drag-me', 'gbSortableData'],
layout: Ember.Handlebars.compile('{{yield}}')
});
moduleForComponent('gb-sortable', 'Sortable', {
setup: function(container){
container.register('component:drag-me', DragMeComponent);
}
});
test('assigning a name', function(){
var component = this.subject({ name: 'omg-lol' });
ok(this.$().hasClass('omg-lol'), 'It should have the `omg-lol` CSS class');
});
test('sorting within itself', function(){
expect(5);
var target = {
itemMoved: function(item, position){
equal(item, 'wtf');
strictEqual(position, 0);
}
};
var component = this.subject({
targetObject: target,
itemMoved: 'itemMoved',
template: Ember.Handlebars.compile(
'{{#drag-me gbSortableData="omg"}}OMG{{/drag-me}}' +
'{{#drag-me gbSortableData="wtf"}}WTF{{/drag-me}}' +
'{{#drag-me gbSortableData="bbq"}}BBQ{{/drag-me}}')
});
this.render();
// We want to drag the second item to the first position
var dy = -1 * this.$('.drag-me').eq(0).outerHeight();
simulate('.drag-me:eq(1)', 'drag', {dy: dy});
andThen(function(){
equal(component.$('.drag-me').eq(0).text().trim(), 'OMG', 'It should not change the DOM');
equal(component.$('.drag-me').eq(1).text().trim(), 'WTF', 'It should not change the DOM');
equal(component.$('.drag-me').eq(2).text().trim(), 'BBQ', 'It should not change the DOM');
});
});
test('restricting the draggable items', function(){
var target = {
itemMoved: function(item, position){
throw 'It should not fire the `itemMoved` action';
}
};
var component = this.subject({
items: '> .drag-me',
targetObject: target,
itemMoved: 'itemMoved',
template: Ember.Handlebars.compile(
'{{#drag-me gbSortableData="omg"}}OMG{{/drag-me}}' +
'<div class="lol">LOL</div>' +
'{{#drag-me gbSortableData="wtf"}}WTF{{/drag-me}}' +
'{{#drag-me gbSortableData="bbq"}}BBQ{{/drag-me}}')
});
this.render();
// We want to drag the second item to the first position
var dy = -1 * this.$('.drag-me').eq(0).outerHeight();
simulate('.lol', 'drag', {dy: dy});
andThen(function(){
equal(component.$('.drag-me').eq(0).text().trim(), 'OMG', 'It should not change the DOM');
equal(component.$('.drag-me').eq(1).text().trim(), 'WTF', 'It should not change the DOM');
equal(component.$('.drag-me').eq(2).text().trim(), 'BBQ', 'It should not change the DOM');
});
});
test('connecting with other sortables', function(){
expect(12);
var target = {
itemRemoved: function(item){
equal(item, 'wtf');
},
itemMoved: function(item, position){
equal(item, 'lol');
}
};
var component = this.subject({
connectWith: '.sortable',
targetObject: target,
itemMoved: 'itemMoved',
itemRemoved: 'itemRemoved',
template: Ember.Handlebars.compile(
'{{#drag-me gbSortableData="omg"}}OMG{{/drag-me}}' +
'{{#drag-me gbSortableData="wtf"}}WTF{{/drag-me}}' +
'{{#drag-me gbSortableData="bbq"}}BBQ{{/drag-me}}')
});
this.render();
var otherSortable = $('<div class="sortable"></div>')
.sortable({
connectWith: '.gb-sortable',
update: function(e, ui){
if(ui.sender){
ui.sender.sortable('cancel');
}
}
})
.append('<div class="lol" data-gb-sortable-data="lol">LOL</div>')
.append('<div class="rofl" data-gb-sortable-data="rofl">ROFL</div>');
$('#ember-testing').append(otherSortable);
var itShouldNotChangeTheDOM = function(){
equal(component.$('.drag-me').eq(0).text().trim(), 'OMG', 'It should not change the DOM');
equal(component.$('.drag-me').eq(1).text().trim(), 'WTF', 'It should not change the DOM');
equal(component.$('.drag-me').eq(2).text().trim(), 'BBQ', 'It should not change the DOM');
equal($('#ember-testing .sortable div').eq(0).text().trim(), 'LOL', 'It should not change the DOM');
equal($('#ember-testing .sortable div').eq(1).text().trim(), 'ROFL', 'It should not change the DOM');
};
// We want to drag 'WTF' to between 'LOL' and 'ROFL'
var dy = this.$('.drag-me').eq(0).outerHeight() * 4;
simulate('.wtf', 'drag', {dy: dy});
andThen(itShouldNotChangeTheDOM);
// We want to drag 'LOL' to between 'WTF' and 'BBQ'
dy = -1 * this.$('.drag-me').eq(0).outerHeight();
simulate('.lol', 'drag', {dy: dy});
andThen(itShouldNotChangeTheDOM);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment