Skip to content

Instantly share code, notes, and snippets.

@stoffeastrom
Last active January 13, 2017 20:01
Show Gist options
  • Save stoffeastrom/f93ae8da5196c4af0019c093e0c4fb73 to your computer and use it in GitHub Desktop.
Save stoffeastrom/f93ae8da5196c4af0019c093e0c4fb73 to your computer and use it in GitHub Desktop.
Aurelia Oribella Sortable Gist New
<template>
<ul oa-sortable="scroll.bind: 'document'; items.bind: items; axis: 'y'">
<li
oa-sortable-item="item.bind: item;"
repeat.for="item of items"
draggable="false">
${item.text}
</li>
</ul>
</template>
export class App {
items = [
{ text: '1' },
{ text: '2' },
{ text: '3' },
{ text: '4' },
{ text: '5' },
{ text: '6' },
{ text: '7' },
{ text: '8' },
{ text: '9' },
{ text: '10' }
];
}
import {transient} from "aurelia-dependency-injection";
@transient()
export class AutoScroll {
rAFId = -1;
speed = 10;
active = false;
start(speed = 10) {
this.speed = speed;
}
update(element, dirX, dirY, frameCntX, frameCntY) {
if (this.active) {
if (dirX === 0 && dirY === 0) {
cancelAnimationFrame(this.rAFId);
this.active = false;
}
return;
}
if (dirX === 0 && dirY === 0) {
return;
}
if (frameCntX === 0 && frameCntY === 0) {
return;
}
const scrollDeltaX = dirX * this.speed;
const scrollDeltaY = dirY * this.speed;
const autoScroll = () => {
if ( !this.active ) {
return;
}
if (frameCntX > 0) {
element.scrollLeft += scrollDeltaX;
}
if (frameCntY > 0) {
element.scrollTop += scrollDeltaY;
}
--frameCntX;
--frameCntY;
if (frameCntX <= 0 && frameCntY <= 0) {
this.active = false;
return;
}
this.rAFId = requestAnimationFrame(autoScroll);
};
this.active = true;
autoScroll();
}
end(cAF = cancelAnimationFrame) {
cAF(this.rAFId);
this.active = false;
}
}
require.config({
baseUrl: ".",
paths: {
},
packages: [
{
name: 'sortable',
location: '.',
main: 'index'
}
],
config: {
es6: { stage: 0 }
}
});
require.load = function(context, moduleName, url) {
require(['es6'], function(es6) {
es6.load(
moduleName,
require,
{
fromText: function(text) {
require.exec(text);
context.completeLoad(moduleName);
}
},
{});
});
};
import {transient} from "aurelia-dependency-injection";
@transient()
export class Drag {
startLeft = 0;
startTop = 0;
rect = { left: 0, top: 0, width: 0, height: 0 };
offsetX = 0;
offsetY = 0;
pin() {
this.item.sortingClass = this.sortingClass;
this.clone = this.element.cloneNode(true);
this.clone.style.position = "absolute";
this.clone.style.width = this.rect.width + "px";
this.clone.style.height = this.rect.height + "px";
this.clone.style.pointerEvents = "none";
this.clone.style.margin = 0;
this.clone.style.zIndex = this.dragZIndex;
document.body.appendChild(this.clone);
}
unpin() {
this.item.sortingClass = "";
document.body.removeChild(this.clone);
this.clone = null;
}
getCenterX() {
return this.rect.left + this.rect.width / 2;
}
getCenterY() {
return this.rect.top + this.rect.height / 2;
}
start(element, item, x, y, viewportScroll, scrollLeft, scrollTop, dragZIndex, axis, sortingClass, minPosX, maxPosX, minPosY, maxPosY) {
this.element = element;
this.item = item;
this.sortingClass = sortingClass;
this.dragZIndex = dragZIndex;
const rect = (this.rect = element.getBoundingClientRect());
this.startLeft = rect.left;
this.startTop = rect.top;
this.offsetX = this.startLeft - x;
this.offsetY = this.startTop - y;
this.pin();
this.update(x, y, viewportScroll, scrollLeft, scrollTop, axis, minPosX, maxPosX, minPosY, maxPosY);
}
update(x, y, viewportScroll, scrollLeft, scrollTop, axis, minPosX, maxPosX, minPosY, maxPosY) {
x += this.offsetX;
y += this.offsetY;
if (viewportScroll) {
x += scrollLeft;
y += scrollTop;
}
if (x < minPosX) {
x = minPosX;
}
if (x > maxPosX - this.rect.width) {
x = maxPosX - this.rect.width;
}
if (y < minPosY) {
y = minPosY;
}
if (y > maxPosY - this.rect.height) {
y = maxPosY - this.rect.height;
}
switch (axis) {
case "x":
y = this.startTop;
break;
case "y":
x = this.startLeft;
break;
}
this.clone.style.left = x + "px";
this.clone.style.top = y + "px";
}
end() {
if (this.element === null) {
return;
}
this.unpin();
this.element = null;
this.item = null;
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="styles.css" rel="stylesheet">
</head>
<body aurelia-app="main">
<h1>Loading...</h1>
<script src="https://jdanyow.github.io/rjs-bundle/node_modules/requirejs/require.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/aurelia.js"></script>
<script src="https://jdanyow.github.io/rjs-bundle/bundles/babel.js"></script>
<script src="https://unpkg.com/oribella/dist/oribella-inline-source-map.js"></script>
<script src="config.js"></script>
<script>
require(['aurelia-bootstrapper']);
</script>
</body>
</html>
export function configure(config) {
config.globalResources("./sortable");
}
export function configure(aurelia) {
aurelia.use
.standardConfiguration()
.developmentLogging()
.plugin('sortable');
aurelia.start().then(() => aurelia.setRoot());
}
import {DOM} from "aurelia-pal";
import {customAttribute, bindable} from "aurelia-templating";
import {inject, transient} from "aurelia-dependency-injection";
import {oribella, Swipe, matchesSelector, GESTURE_STRATEGY_FLAG} from 'oribella';
import {Drag} from "./drag";
import {AutoScroll} from "./auto-scroll";
const SORTABLE_ITEM = "oa-sortable-item";
@customAttribute("oa-sortable")
@inject(DOM.Element, Drag, AutoScroll)
@transient()
export class Sortable {
@bindable scroll = null;
@bindable scrollSpeed = 10;
@bindable scrollSensitivity = 10;
@bindable items = [];
@bindable sortingClass = "oa-sorting";
@bindable draggingClass = "oa-dragging";
@bindable axis = "";
@bindable moved = () => {};
@bindable dragZIndex = 1;
@bindable disallowedDragTagNames = ["INPUT", "SELECT", "TEXTAREA"];
@bindable allowDrag = args => {
if (this.disallowedDragTagNames.indexOf(args.event.target.tagName) !== -1) {
return false;
}
if (args.event.target.isContentEditable) {
return false;
}
return true;
};
@bindable allowMove = () => { return true; };
selector = "[" + SORTABLE_ITEM + "]";
fromIx = -1;
toIx = -1;
x = 0;
y = 0;
lastElementFromPointRect = null;
constructor(element, drag, autoScroll) {
this.element = element;
this.drag = drag;
this.autoScroll = autoScroll;
this.options = {
strategy: GESTURE_STRATEGY_FLAG.REMOVE_IF_POINTERS_GT
};
}
activate() {
this.removeListener = oribella.on(Swipe, this.element, this);
let scroll = this.scroll;
if (typeof scroll === "string") {
if (scroll === "document") {
this.scroll = document.scrollingElement || document.documentElement || document.body;
this.viewportScroll = true;
this.removeScroll = this.bindScroll(document, this.onScroll.bind(this));
return;
} else {
scroll = this.closest(this.element, scroll);
}
}
this.scroll = scroll;
if (!(this.scroll instanceof DOM.Element)) {
this.scroll = this.element;
}
this.removeScroll = this.bindScroll(this.scroll, this.onScroll.bind(this));
}
deactivate() {
if (typeof this.removeListener === "function") {
this.removeListener();
}
if (typeof this.removeScroll === "function") {
this.removeScroll();
}
}
attached() {
this.activate();
}
detached() {
this.deactivate();
}
bindScroll(scroll, fn) {
scroll.addEventListener("scroll", fn, false);
return () => {
scroll.removeEventListener("scroll", fn, false);
};
}
onScroll() {
if (!this.drag.element) {
return;
}
const { scrollLeft, scrollTop } = this.scroll;
this.drag.update(this.x,
this.y,
this.viewportScroll,
scrollLeft,
scrollTop,
this.axis,
this.dragMinPosX,
this.dragMaxPosX,
this.dragMinPosY,
this.dragMaxPosY);
const { x, y } = this.getPoint(this.x, this.y);
this.tryMove(x, y, scrollLeft, scrollTop);
}
getScrollFrames(maxPos, scrollPos) {
return Math.max(0, Math.ceil(Math.abs(maxPos - scrollPos) / this.scrollSpeed));
}
getScrollDirectionX(x, { left, right }) {
let dir = 0;
switch (this.axis) {
default:
case "x":
if (x >= right - this.scrollSensitivity) {
dir = 1;
} else if (x <= left + this.scrollSensitivity) {
dir = -1;
}
break;
}
return dir;
}
getScrollDirectionY(y, { top, bottom }) {
let dir = 0;
switch (this.axis) {
default:
case "y":
if (y >= bottom - this.scrollSensitivity) {
dir = 1;
} else if (y <= top + this.scrollSensitivity) {
dir = -1;
}
break;
}
return dir;
}
hide(element) {
const display = element.style.display;
element.style.display = "none";
return () => {
element.style.display = display;
};
}
closest(element, selector, rootElement = document) {
var valid = false;
while (!valid && element !== null && element !== rootElement &&
element !== document) {
valid = matchesSelector(element, selector);
if (valid) {
break;
}
element = element.parentNode;
}
return valid ? element : null;
}
getItemViewModel(element) {
return element.au[SORTABLE_ITEM].viewModel;
}
moveSortingItem(toIx) {
const fromIx = this.items.indexOf(this.drag.item);
this.toIx = toIx;
this.move(fromIx, toIx);
}
move(fromIx, toIx) {
if (fromIx !== -1 && toIx !== -1 && fromIx !== toIx) {
this.items.splice(toIx, 0, this.items.splice(fromIx, 1)[0]);
}
}
tryUpdate(x, y, offsetX, offsetY) {
const showFn = this.hide(this.drag.clone);
this.tryMove(x, y, offsetX, offsetY);
showFn();
}
pointInside(x, y, rect) {
return x >= rect.left &&
x <= rect.right &&
y >= rect.top &&
y <= rect.bottom;
}
elementFromPoint(x, y) {
let element = document.elementFromPoint(x, y);
if (!element) {
return null;
}
element = this.closest(element, this.selector, this.element);
if (!element) {
return null;
}
return element;
}
canThrottle(x, y, offsetX, offsetY) {
return this.lastElementFromPointRect &&
this.pointInside(x + offsetX, y + offsetY, this.lastElementFromPointRect);
}
tryMove(x, y, offsetX, offsetY) {
if (this.canThrottle(x, y, offsetX, offsetY)) {
return;
}
const element = this.elementFromPoint(x, y);
if (!element) {
return;
}
const vm = this.getItemViewModel(element);
this.lastElementFromPointRect = element.getBoundingClientRect();
if (!this.allowMove({ item: vm.item })) {
return;
}
this.moveSortingItem(vm.ctx.$index);
}
getPoint(pageX, pageY) {
switch (this.axis) {
case "x":
pageY = this.drag.getCenterY();
break;
case "y":
pageX = this.drag.getCenterX();
break;
default:
break;
}
return {
x: pageX,
y: pageY
};
}
down(e, data, element) {
if (this.allowDrag({ event: e, item: this.getItemViewModel(element).item })) {
e.preventDefault();
return undefined;
}
return false;
}
start(e, data, element) {
const { scrollLeft, scrollTop } = this.scroll;
this.windowHeight = innerHeight;
this.windowWidth = innerWidth;
this.x = data.pointers[0].client.x;
this.y = data.pointers[0].client.y;
this.sortableRect = this.element.getBoundingClientRect();
this.scrollRect = this.scroll.getBoundingClientRect();
this.scrollWidth = this.scroll.scrollWidth;
this.scrollHeight = this.scroll.scrollHeight;
this.boundingRect = {
left: Math.max(0, this.sortableRect.left),
top: Math.max(0, this.sortableRect.top),
bottom: Math.min(this.windowHeight, this.sortableRect.bottom),
right: Math.min(this.windowWidth, this.sortableRect.right)
};
this.sortableContainsScroll = this.element.contains(this.scroll);
if (this.sortableContainsScroll) {
this.scrollMaxPosX = this.scrollWidth - this.scrollRect.width;
this.scrollMaxPosY = this.scrollHeight - this.scrollRect.height;
this.dragMinPosX = this.sortableRect.left;
this.dragMaxPosX = this.sortableRect.left + this.scrollWidth;
this.dragMaxPosY = this.sortableRect.top + this.scrollHeight;
this.dragMinPosY = this.sortableRect.top;
} else {
this.scrollMaxPosX = this.sortableRect.right - this.windowWidth + (this.viewportScroll ? scrollLeft : 0);
this.scrollMaxPosY = this.sortableRect.bottom - this.windowHeight + (this.viewportScroll ? scrollTop : 0);
this.dragMinPosX = this.sortableRect.left + scrollLeft;
this.dragMaxPosX = this.scrollMaxPosX + this.windowWidth;
this.dragMinPosY = this.sortableRect.top + scrollTop;
this.dragMaxPosY = this.scrollMaxPosY + this.windowHeight;
}
this.sortingViewModel = this.getItemViewModel(element);
this.fromIx = this.sortingViewModel.ctx.$index;
this.toIx = -1;
this.drag.start( element,
this.sortingViewModel.item,
this.x, this.y,
this.viewportScroll,
scrollLeft,
scrollTop,
this.dragZIndex,
this.axis, this.sortingClass,
this.dragMinPosX,
this.dragMaxPosX,
this.dragMinPosY,
this.dragMaxPosY);
this.autoScroll.start(this.scrollSpeed);
this.lastElementFromPointRect = this.drag.rect;
}
update(e, data) {
const p = data.pointers[0].client;
const { scrollLeft, scrollTop } = this.scroll;
this.x = p.x;
this.y = p.y;
this.drag.update(this.x,
this.y,
this.viewportScroll,
scrollLeft,
scrollTop,
this.axis,
this.dragMinPosX,
this.dragMaxPosX,
this.dragMinPosY,
this.dragMaxPosY);
const { x, y } = this.getPoint(p.x, p.y);
const scrollX = this.autoScroll.active ? scrollLeft : 0;
const scrollY = this.autoScroll.active ? scrollTop : 0;
this.tryUpdate(x, y, scrollX, scrollY);
const dirX = this.getScrollDirectionX(x, this.boundingRect);
const dirY = this.getScrollDirectionY(y, this.boundingRect);
let frameCntX = this.getScrollFrames(dirX === -1 ? 0 : this.scrollMaxPosX, scrollLeft);
let frameCntY = this.getScrollFrames(dirY === -1 ? 0 : this.scrollMaxPosY, scrollTop);
if ((dirX === 1 && scrollLeft >= this.scrollMaxPosX) ||
(dirX === -1 && scrollLeft === 0)) {
frameCntX = 0;
}
if ((dirY === 1 && scrollTop >= this.scrollMaxPosY) ||
(dirY === -1 && scrollTop === 0)) {
frameCntY = 0;
}
this.autoScroll.update(this.scroll, dirX, dirY, frameCntX, frameCntY);
}
end() {
if (!this.drag.item) {
return; //cancelled
}
this.stop();
if (this.fromIx !== this.toIx) {
this.moved( { fromIx: this.fromIx, toIx: this.toIx } );
}
}
cancel() {
this.move(this.sortingViewModel.ctx.$index, this.fromIx);
this.stop();
}
stop() {
this.drag.end();
this.autoScroll.end();
}
}
@customAttribute("oa-sortable-item")
export class SortableItem {
@bindable item = null;
bind(ctx, overrideCtx) {
this.ctx = overrideCtx; //Need a reference to the item's $index
}
}
html, body {
padding: 8px;
margin: 0;
}
ul {
padding: 0;
margin: 0;
list-style: none;
}
li {
box-sizing: border-box;
margin: 4px 0px;
border: 1px solid #999;
line-height: 2rem;
text-align: center;
list-style: none;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment