Skip to content

Instantly share code, notes, and snippets.

@chrism
Forked from spruce/controllers.application\.js
Last active August 31, 2020 16:31
Show Gist options
  • Save chrism/8bef8fc40de87e5d47566e06b97e8949 to your computer and use it in GitHub Desktop.
Save chrism/8bef8fc40de87e5d47566e06b97e8949 to your computer and use it in GitHub Desktop.
Drag Drop Ember Animated
import Controller from '@ember/controller';
import { action } from "@ember/object";
import { tracked } from '@glimmer/tracking';
import move from 'ember-animated/motions/move';
import { inject as service } from "@ember/service";
import drag from '../motions/drag';
export default class ApplicationController extends Controller {
@service("-ea-motion") motion;
@tracked animators = 'click button above';
appName = 'Ember Twiddle';
allTodos = [createTodo(2,'todo'),createTodo(1,'todo'),createTodo(3,'todo'),
createTodo(4,'todo'),createTodo(5,'todo'),createTodo(6,'todo'),
createTodo(7,'doing'),createTodo(8,'doing'),createTodo(9,'doing')];
get todo(){
return this.allTodos.filterBy("state", "todo");
};
get sortedTodo() {
return this.todo.sortBy("sortPriority");
};
get doing(){
return this.allTodos.filterBy("state", "doing");
};
get sortedDoing() {
return this.doing.sortBy("sortPriority");
};
@action
logAnimators(task) {
this.animators = `this.motion._animators[1].finalizeAnimation.isRunning: ${this.motion._animators[1].finalizeAnimation.isRunning}<br>this.motion._animators[3].finalizeAnimation.isRunning:${this.motion._animators[3].finalizeAnimation.isRunning}`
console.log(this.motion._animators[1].finalizeAnimation.isRunning);
console.log(this.motion._animators[3].finalizeAnimation.isRunning);
};
@action
beginDragging(task, event) {
console.log('beginDragging', task, event);
let dragState;
let self = this;
function stopMouse() {
recalc(self.sortedTodo);
recalc(self.sortedDoing);
task.dragState = null;
Ember.notifyPropertyChange(task, "dragState");
window.removeEventListener("mouseup", stopMouse);
window.removeEventListener("mousemove", updateMouse);
}
function updateMouse(event) {
dragState.latestPointerX = event.x;
dragState.latestPointerY = event.y;
task.dragState = dragState;
Ember.notifyPropertyChange(task, "dragState");
}
dragState = new DragState({
initialPointerX: event.x,
initialPointerY: event.y,
latestPointerX: event.x,
latestPointerY: event.y,
column: task.state,
});
window.addEventListener("mouseup", stopMouse);
window.addEventListener("mousemove", updateMouse);
task.dragState = dragState;
Ember.notifyPropertyChange(task, "dragState");
};
get otherColumns(){
return this._columns;
};
_columns = [];
@action
registerColumn(element, args) {
setTimeout(() => {this._columns.push({
name: args[0],
x: element.offsetLeft + element.offsetWidth / 2,
});
this.notifyPropertyChange("otherColumns");}, 500)
};
transition = function* (otherColumns, obj) {
console.log('transition')
let { keptSprites, receivedSprites } = obj;
let activeSprite = keptSprites
.concat(receivedSprites)
.find((sprite) => sprite.owner.value.dragState);
let others = keptSprites.filter((sprite) => sprite !== activeSprite);
if (activeSprite) {
drag(activeSprite, {
others,
otherColumns,
onCollision(otherSprite) {
console.log('collision');
// same column
let myModel = activeSprite.owner.value;
let otherModel = otherSprite.owner.value;
let myPriority = myModel.sortPriority;
let theirPriority = otherModel.sortPriority;
// if we are not neighbors make it wonky
if (myPriority > theirPriority) {
myModel.sortPriority = theirPriority - 0.5;
} else {
myModel.sortPriority = theirPriority + 0.5;
}
Ember.notifyPropertyChange(myModel, "sortPriority");
},
onCollisionOther(otherColumn) {
console.log('onCollisionOther')
let myModel = activeSprite.owner.value;
myModel.state = otherColumn;
},
});
}
others.forEach(move);
}
}
function createTodo(num, state){
return new Todo({
name: state + num,
state,
id: num,
sortPriority: num,
dragState: null
})
}
function sleep(ms){
return new Promise((resolve) => {setTimeout(resolve, ms);});
}
class Todo{
@tracked name;
@tracked state;
@tracked id;
@tracked sortPriority;
@tracked dragState;
constructor(obj){
this.name = obj.name;
this.state = obj.state;
this.id = obj.id;
this.sortPriority = obj.sortPriority;
this.dragState = obj.dragState;
}
}
class DragState{
@tracked initialPointerX;
@tracked initialPointerY;
@tracked latestPointerX;
@tracked latestPointerY;
@tracked column;
constructor(obj){
this.initialPointerX = obj.initialPointerX;
this.initialPointerY = obj.initialPointerY;
this.latestPointerX = obj.latestPointerX;
this.latestPointerY = obj.latestPointerY;
this.column = obj.column;
}
}
function recalc(columns) {
console.log(columns)
let counter = 1;
for (const task of columns) {
task.sortPriority = counter++;
}
}
// copied from https://github.com/ef4/living-animation/blob/master/src/ui/routes/tutorial-24/-utils/drag.js
import { Motion, rAF } from "ember-animated";
import { next } from "@ember/runloop";
export default function drag(sprite, opts) {
console.log('new drag');
return new Drag(sprite, opts).run();
}
class Drag extends Motion {
constructor(sprite, opts) {
super(sprite, opts);
this.canChange = true;
this.prior = null;
// This is our own sprite's absolute screen position that
// corresponds to the real start of dragging (which may span many
// Drag instances, because of interruption)
this.dragStartX = null;
this.dragStartY = null;
}
interrupted(motions) {
this.prior = motions.find((m) => m instanceof this.constructor);
}
*animate() {
console.log('animate', this.opts.otherColumns);
let sprite = this.sprite;
let initialTx, initialTy;
if (this.prior) {
this.dragStartX = this.prior.dragStartX;
this.dragStartY = this.prior.dragStartY;
initialTx =
sprite.transform.tx -
sprite.absoluteInitialBounds.left +
this.dragStartX;
initialTy =
sprite.transform.ty -
sprite.absoluteInitialBounds.top +
this.dragStartY;
} else {
this.dragStartX = sprite.absoluteInitialBounds.left;
this.dragStartY = sprite.absoluteInitialBounds.top;
initialTx = sprite.transform.tx;
initialTy = sprite.transform.ty;
}
// targets are all in absolute screen coordinates
let targets = this.opts.others.map((s) =>
makeTarget(s.absoluteFinalBounds, s)
);
let targetCols = this.opts.otherColumns;
let ownTarget = makeTarget(sprite.absoluteFinalBounds, sprite);
let dragState = sprite.owner.value.dragState;
sprite.applyStyles({
"z-index": "1",
outline: "none",
});
while (sprite.owner.value.dragState) {
let dragState = sprite.owner.value.dragState;
// these track relative motion since the drag started
let dx = dragState.latestPointerX - dragState.initialPointerX;
let dy = dragState.latestPointerY - dragState.initialPointerY;
// adjust our transform to match the latest relative mouse motion
sprite.translate(
dx + initialTx - sprite.transform.tx,
dy + initialTy - sprite.transform.ty
);
// now this is our own absolute center position
let x = dx + this.dragStartX + sprite.absoluteFinalBounds.width / 2;
let y = dy + this.dragStartY + sprite.absoluteFinalBounds.height / 2;
let ownDistance =
(x - ownTarget.x) * (x - ownTarget.x) +
(y - ownTarget.y) * (y - ownTarget.y);
let closerTarget = targets.find((target) => {
let partialX = target.x - x;
let partialY = target.y - y;
let distance = partialX * partialX + partialY * partialY;
return distance < ownDistance;
});
let ownXDistance = Math.abs(x - ownTarget.x); // distance to where it will animate to
let closerOtherColumn = targetCols.find((col) => {
return (
col.name !== sprite.owner.value.state &&
Math.abs(col.x - x) + 10 < ownXDistance
);
});
if (closerOtherColumn) {
next(this, this.opts.onCollisionOther, closerOtherColumn.name);
} else if (closerTarget) {
next(this, this.opts.onCollision, closerTarget.payload);
}
yield rAF();
}
}
}
export function makeTarget(bounds, payload) {
return {
x: bounds.left + bounds.width / 2,
y: bounds.top + bounds.height / 2,
payload,
};
}
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
<style>.unselectable{
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}</style>
<div class="flex mt-16">
<div
class="flex-1 p-3 m-3 bg-gray-300"
{{did-insert this.registerColumn "todo"}}
>
<AnimatedContainer>
{{#animated-each
sortedTodo
use=(fn this.transition this.otherColumns)
duration=300
watch="dragState" as |task|
}}
<div
class="h-10 mt-2 p-2 w-100 bg-gray-600 unselectable {{task.name}}"
tabindex="-1"
{{on "mousedown" (fn this.beginDragging task)}}
>
<h3>
{{task.name}}
{{task.dragState}}
{{task.sortPriority}}
</h3>
</div>
{{/animated-each}}
</AnimatedContainer>
</div>
<div
class="flex-1 p-3 m-3 bg-gray-300"
{{did-insert this.registerColumn "doing"}}>
<AnimatedContainer>
{{#animated-each
sortedDoing
use=(fn this.transition this.otherColumns)
duration=300
watch="dragState" as |task|
}}
<div
class="h-10 mt-2 p-2 w-100 bg-gray-600 unselectable {{task.name}}"
tabindex="-1"
{{on "mousedown" (fn this.beginDragging task)}}
>
<h3>
{{task.name}}
</h3>
</div>
{{/animated-each}}
</AnimatedContainer>
</div>
</div>
<ol>
<li>
Drag one item from todo down the list.
</li>
<li>
It should smoothly go there and the animators finalizeMethod finishes. (check by clicking the button)
</li>
<li>But drag it to the other column and animations are presented as never finishing.(check by clicking the button)</li>
</ol>
<br>
Animators: <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded" {{on "click" this.logAnimators}}>logAnimators</button><br>
{{{this.animators}}}
{
"version": "0.17.1",
"EmberENV": {
"FEATURES": {},
"_TEMPLATE_ONLY_GLIMMER_COMPONENTS": false,
"_APPLICATION_TEMPLATE_WRAPPER": true,
"_JQUERY_INTEGRATION": true
},
"options": {
"use_pods": false,
"enable-testing": false
},
"dependencies": {
"jquery": "https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.js",
"ember": "3.18.1",
"ember-template-compiler": "3.18.1",
"ember-testing": "3.18.1"
},
"addons": {
"@glimmer/component": "1.0.0",
"ember-animated": "0.10.1",
"@ember/render-modifiers": "1.0.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment