mardi 27 janvier 2015

Moving Ember Metal Views without re-rendering (drag and drop)

We leverage the draggable-each project to implement drag and drop functionality where we avoid re-rendering the moved views. I am trying to get this to work with Ember 1.9 and metal views. I have been trying to fix up the morphs without triggering a re-render of the child views. This works with dragging within one draggable-each view but not when dragging between them or inserting a new draggable-each. The views get re-rendered and I have yet to figure out why.


Could someone give some insight into how rendering is done in metal views and the best way to move views without triggering a re-rendering?


Thanks.


My current version is:



define('components/draggable-each', [], function () {

var a_slice = Array.prototype.slice;

var get = Ember.get;

return Ember.CollectionView.extend(Ember.TargetActionSupport, {
classNames: ['ember-drag-list'],
content: Ember.computed('context', function () {
return this.get('context');
}),
handleSelector: null,
itemSelector: '.draggable-item',
target: Ember.computed.oneWay('controller'),
init: function () {
var itemView = this.get('itemView');
var ItemViewClass;

if (itemView) {
ItemViewClass = this.container.lookupFactory('view:' + itemView);
} else {
ItemViewClass = this.get('itemViewClass');
}

this.set('itemViewClass', ItemViewClass.extend({
context: Ember.computed.oneWay('content'),
template: this.get('template'),
classNames: ['draggable-item']
}));

this.updateRefCount = 0;

this._super.apply(this, arguments);
},

nonVirtualChildViews: function () {
var children = this._childViews;
var nonVirtualChildren = [];

for (var i = 0; i < children.length; i++) {
var grandchildren = children[i].get('childViews');
if (grandchildren) {
for (var y = 0; y < grandchildren.length; y++) {
nonVirtualChildren.push(grandchildren[y]);
}
}
}

return nonVirtualChildren;
},

// lifted from Ember.Compontent
sendAction: function (action) {
var actionName;
var contexts = a_slice.call(arguments, 1);

// Send the default action
if (action === undefined) {
actionName = get(this, 'action');
} else {
actionName = get(this, action);
}

// If no action name for that action could be found, just abort.
if (actionName === undefined) { return; }

this.triggerAction({
action: actionName,
actionContext: contexts
});
},

viewReceived: function (view /*, source */) {
view.set('parentView', this);
//view.set('parentView', this.get('parentView'));
view.set('_parentView', this);
},

arrayWillChange: function () {
if (this.updateDisabled()) { return; }
this._super.apply(this, arguments);
},

arrayDidChange: function () {
if (this.updateDisabled()) { return; }
this._super.apply(this, arguments);
},

updateDisabled: function () {
return this.updateRefCount > 0;
},

execWithoutRerender: function (func) {
this.updateRefCount++;

try {
return func();
} finally {
this.updateRefCount--;
}
},

// Note: entryView is optional--if not provided, view will render anew from entry
// skipNotify is also optional, and if not present, the itemWasInserted action will be sent
// NOTE: make sure to call viewReceived outside of "updateDisabled" block if entryView already exists (i.e. is being moved from elsewhere)
insertItem: function (index, entry, entryView, skipNotify, eventContext) {
var list = this.get('context');
if (entryView) {
console.log('+++ adding view ' + entryView.get('elementId'));
} else {
console.log('+++ adding new view');
}

if (entryView) {
//insert the view
this._insertView(entryView, index);
//update the DOM to reflect
//index > 0 ? entryView.$().insertAfter(this._childViews[index - 1].$()) : this.$().prepend(entryView.$());
}

list.insertAt(index, entry);

if (!skipNotify) {
this.sendAction('itemWasInserted', entry, index, list, eventContext);
}
},

// skipNotify is optional, and if not present, the itemWasInserted action will be sent
removeItem: function (index, isDelete, skipNotify, eventContext) {
var list = this.get('context');

var object = list.objectAt(index);
var entry = object.isController ? object.get('content') : object;
var view = this._childViews[index];
var elementId = view.get('elementId');

console.log('--- removing view ' + elementId);

//only remove from the childViews array once--if update is not disabled, then the view will be destroyed upon removing from the model,
//and thus also removed from the views list... so if we remove here as well as in listeners, we might remove multiple views
if (this.updateDisabled()) {
this._removeView(view, index);
}
/*
if (!isDelete) {
//if it is not a delete, we need to do a $().detach() to prevent data from being deferenced for GC
view.$().detach();
}
else if (this.updateDisabled()) {
view.remove();
}
*/
//remove from the backing array model, causing the view to be removed if update is not disabled
list.removeAt(index);
Ember.propertyDidChange(this, 'childViews');

if (!skipNotify) {
this.sendAction('itemWasRemoved', entry, index, list, eventContext);
}

return {
entryModel: entry,
entryView: view
};
},

replaceItem: function (index, replacement, skipNotify, eventContext) {
var list = this.get('context');

var object = list.objectAt(index);
var entry = object.isController ? object.get('content') : object;
var view = this._childViews[index];

//remove from the backing array model, causing the view to be removed if update is not disabled,
//and add in the replacement at that same location
list.replace(index, 1, [replacement]);

if (!skipNotify) {
this.sendAction('itemWasReplaced', entry, index, list, eventContext);
}

return {
entryModel: entry,
entryView: view
};
},

moveItem: function (oldIndex, newIndex, source, eventContext) {
var self = this;
var sourceList = source.get('content');
var targetList = this.get('content');

var object = sourceList.objectAt(oldIndex);
var entry = object.isController ? object.get('content') : object;
//var viewToMove = source._childViews[oldIndex];
//source._renderer.remove(view, false, true);
//var view = source._childViews.splice(oldIndex, 1)[0];

var doMove = function () {
var viewToMove = source._childViews.splice(oldIndex, 1)[0];
var resolvedNewIndex = newIndex + (self === source && oldIndex < newIndex ? -1 : 0);

self._insertView(viewToMove, resolvedNewIndex);
source._removeView(viewToMove, oldIndex);

sourceList.removeAt(oldIndex);
targetList.insertAt(resolvedNewIndex, entry);
viewToMove.set('content', targetList.objectAt(resolvedNewIndex)); // needed when using item controllers that will get destroyed subsequent to the removeAt operation

Ember.propertyDidChange(source, 'childViews');
Ember.propertyDidChange(self, 'childViews');

//var info = source.removeItem(oldIndex, false, true);
//that.insertItem(resolvedNewIndex, info.entryModel, info.entryView, true);

//return info;

return {
entryModel: entry,
entryView: viewToMove
};
};

var info = this.execWithoutRerender(function () {
return source.execWithoutRerender(doMove, this);
}, this);


this.sendAction('itemWasMoved', info.entryModel, oldIndex, newIndex, source, eventContext);
},

// tell morph shadow DOM that child view should have this draggable-each as
// parent view now
_insertView: function (view, newIndex) {
//set the parentView of the new child
this.viewReceived(view);
view._morph = this._childViewsMorph.insert(newIndex, view.element);
this._childViews.splice(newIndex, 0, view);
},

_removeView: function(view, index) {
this._childViewsMorph.removeMorph(view._morph);
this._childViews.splice(index, 1);
}
});
});




Aucun commentaire:

Enregistrer un commentaire