Skip to content

Instantly share code, notes, and snippets.

@stoffeastrom
Last active January 11, 2017 20:17
Show Gist options
  • Save stoffeastrom/45b6d7d71155a61acf10218fb67f8d0f to your computer and use it in GitHub Desktop.
Save stoffeastrom/45b6d7d71155a61acf10218fb67f8d0f to your computer and use it in GitHub Desktop.
Aurelia Oribella Sortable Table Rows Gist Old Sortable
<template>
<table sortable="scroll.bind: 'document'; items.bind: items; axis: 'y'">
<tbody>
<tr
view-cache="*"
sortable-item="item.bind: item;"
repeat.for="item of items"
draggable="false">
<td>
<div>${item.text}</div>
</td>
</tr>
</tbody>
</table>
</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 {
ticks = [0, 0];
rAFId = -1;
axis = "";
speed = 10;
sensitivity = 10;
active = false;
start(axis = "", speed = 10, sensitivity = 10) {
this.axis = axis;
this.speed = speed;
this.sensitivity = sensitivity;
}
update(element, x, y, scrollWidth, scrollHeight, scrollRect, rAF = requestAnimationFrame, cAF = cancelAnimationFrame) {
const d = this.getScrollDirection(x, y, scrollRect);
if(this.active) {
if(d[0] === 0 && d[1] === 0) {
cAF(this.rAFId);
this.active = false;
}
return;
}
if(d[0] === 0 && d[1] === 0) {
return;
}
this.ticks = this.getTicks(d, element.scrollLeft, element.scrollTop, scrollWidth, scrollHeight, scrollRect.width, scrollRect.height);
if(this.ticks[0] <= 0 && this.ticks[1] <= 0) {
return;
}
const scrollDeltaX = d[0] * this.speed;
const scrollDeltaY = d[1] * this.speed;
const autoScroll = () => {
if(this.ticks[0] > 0) {
element.scrollLeft += scrollDeltaX;
}
if(this.ticks[1] > 0) {
element.scrollTop += scrollDeltaY;
}
--this.ticks[0];
--this.ticks[1];
if (this.ticks[0] <= 0 && this.ticks[1] <= 0) {
this.active = false;
return;
}
this.rAFId = rAF(autoScroll);
};
this.active = true;
autoScroll();
}
end(cAF = cancelAnimationFrame) {
cAF(this.rAFId);
this.ticks = [0, 0];
}
getTicks(d, scrollLeft, scrollTop, scrollWidth, scrollHeight, width, height) {
const ticks = [];
ticks[0] = d[0] > 0 ?
Math.ceil((scrollWidth - width - scrollLeft) / this.speed) :
d[0] < 0 ? scrollLeft / this.speed : 0;
ticks[1] = d[1] > 0 ?
Math.ceil((scrollHeight - height - scrollTop) / this.speed) :
d[1] < 0 ? scrollTop / this.speed : 0;
return ticks;
}
getScrollDirection(x, y, scrollRect) {
const { left, top, right, bottom = 0 } = scrollRect;
const d = [0, 0];
d[0] = this.axis === "y" ? 0 :
(x >= right - this.sensitivity) ? 1 :
(x <= left + this.sensitivity) ? -1 : 0;
d[1] = this.axis === "x" ? 0 :
(y >= bottom - this.sensitivity) ? 1 :
(y <= top + this.sensitivity) ? -1 : 0;
return d;
}
}
require.config({
baseUrl: ".",
paths: {
},
packages: [
{
name: 'oribella-framework',
location: 'https://unpkg.com/oribella-framework/dist/amd',
main: 'index'
},
{
name: 'oribella',
location: 'https://unpkg.com/oribella/dist/amd',
main: 'index'
},
{
name: 'sortable',
location: '.',
main: 'index'
}
],
config: {
es6: { stage: 0 }
}
});
require.standardLoad = require.load;
require.load = function(context, moduleName, url) {
var config = requirejs.s.contexts._.config;
if (moduleName in config.paths) {
return require.standardLoad(context, moduleName, url);
}
if (moduleName.indexOf('oribella') !== -1) {
return require.standardLoad(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(element, rect, dragZIndex) {
this.element = element;
let style = {};
style.position = element.style.position;
style.left = element.style.left;
style.top = element.style.top;
style.width = element.style.width;
style.height = element.style.height;
style.pointerEvents = element.style.pointerEvents;
style.zIndex = element.style.zIndex;
element.style.position = "absolute";
element.style.width = rect.width + "px";
element.style.height = rect.height + "px";
element.style.pointerEvents = "none";
element.style.zIndex = dragZIndex;
return () => {
Object.keys( style ).forEach( key => {
element.style[key] = style[key];
} );
};
}
getCenterX() {
return this.rect.left + this.rect.width / 2;
}
getCenterY() {
return this.rect.top + this.rect.height / 2;
}
start(element, pageX, pageY, scrollLeft, scrollTop, dragZIndex) {
const rect = (this.rect = element.getBoundingClientRect());
const offsetParentRect = element.offsetParent.getBoundingClientRect();
this.startLeft = rect.left;// - offsetParentRect.left;
this.startTop = rect.top;// - offsetParentRect.top;
this.offsetX = this.startLeft - pageX - scrollLeft;
this.offsetY = this.startTop - pageY - scrollTop;
this.unpin = this.pin(element, rect, dragZIndex);
this.update(pageX, pageY, scrollLeft, scrollTop);
}
update(pageX, pageY, scrollLeft, scrollTop, axis) {
let left = pageX + this.offsetX + scrollLeft;
let top = pageY + this.offsetY + scrollTop;
switch (axis) {
case "x":
top = this.startTop;
break;
case "y":
left = this.startLeft;
break;
default:
break;
}
this.element.style.left = left + "px";
this.element.style.top = top + "px";
}
end() {
if (this.element) {
if(typeof this.unpin === "function") {
this.unpin();
}
this.element = 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="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 {matchesSelector, GESTURE_STRATEGY_FLAG} from "oribella-framework";
import {oribella, Swipe} from 'oribella';
import {Drag} from "./drag";
import {AutoScroll} from "./auto-scroll";
export const PLACEHOLDER = "__placeholder__";
@customAttribute("sortable")
@inject(DOM.Element, Drag, AutoScroll)
@transient()
export class Sortable {
@bindable scroll = null;
@bindable scrollSpeed = 10;
@bindable scrollSensitivity = 10;
@bindable items = [];
@bindable placeholderClass = "placeholder";
@bindable axis = "";
@bindable boundingRect = null; //{ left, top, right, bottom }
@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;
pageX = 0;
pageY = 0;
scrollRect = { left: 0, top: 0, width: 0, height: 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);
if (typeof this.scroll === "string") {
this.scroll = this.closest(this.element, this.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 = this.scroll.scrollLeft;
const scrollTop = this.scroll.scrollTop;
this.drag.update(this.pageX, this.pageY, scrollLeft, scrollTop, this.axis);
const { x, y } = this.getPoint(this.pageX, this.pageY);
this.tryMove(x, y, scrollLeft, scrollTop);
}
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;
}
addPlaceholder(toIx, item) {
let placeholder = Object.create(item, { placeholderClass: { value: this.placeholderClass, writable: true }});
if (!placeholder.style) {
placeholder.style = {};
}
placeholder.style.width = this.drag.rect.width;
placeholder.style.height = this.drag.rect.height;
this[PLACEHOLDER] = placeholder;
this.items.splice(toIx, 0, placeholder);
}
removePlaceholder() {
const ix = this.items.indexOf(this[PLACEHOLDER]);
if (ix !== -1) {
this.items.splice(ix, 1);
}
}
movePlaceholder(toIx) {
const fromIx = this.items.indexOf(this[PLACEHOLDER]);
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(pageX, pageY, offsetX, offsetY) {
const showFn = this.hide(this.drag.element);
this.tryMove(pageX, pageY, 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 model = this.getItemViewModel(element);
this.lastElementFromPointRect = element.getBoundingClientRect();
if (!this.allowMove({ item: model.item })) {
return;
}
const ix = model.ctx.$index;
this.movePlaceholder(ix);
}
getPoint(pageX, pageY) {
switch (this.axis) {
case "x":
pageY = this.drag.getCenterY();
break;
case "y":
pageX = this.drag.getCenterX();
break;
default:
break;
}
return {
x: Math.max(this.boundingRect.left, Math.min(this.boundingRect.right, pageX)),
y: Math.max(this.boundingRect.top, Math.min(this.boundingRect.bottom, 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) {
this.pageX = data.pointers[0].client.x;
this.pageY = data.pointers[0].client.y;
this.scrollRect = this.scroll.getBoundingClientRect();
this.scrollWidth = this.scroll.scrollWidth;
this.scrollHeight = this.scroll.scrollHeight;
this.boundingRect = this.boundingRect || { left: this.scrollRect.left + 5, top: this.scrollRect.top + 5, right: this.scrollRect.right - 5, bottom: this.scrollRect.bottom - 5 };
this.drag.start(element, this.pageX, this.pageY, this.scroll.scrollLeft, this.scroll.scrollTop, this.dragZIndex);
this.autoScroll.start(this.axis, this.scrollSpeed, this.scrollSensitivity);
const viewModel = this.getItemViewModel(element);
this.fromIx = viewModel.ctx.$index;
this.toIx = -1;
this.addPlaceholder(this.fromIx, viewModel.item);
this.lastElementFromPointRect = this.drag.rect;
}
update(e, data) {
const p = data.pointers[0].client;
const pageX = (this.pageX = p.x);
const pageY = (this.pageY = p.y);
const scrollLeft = this.scroll.scrollLeft;
const scrollTop = this.scroll.scrollTop;
this.drag.update(pageX, pageY, scrollLeft, scrollTop, this.axis);
const { x, y } = this.getPoint(pageX, pageY);
const scrollX = this.autoScroll.active ? scrollLeft : 0;
const scrollY = this.autoScroll.active ? scrollTop : 0;
this.tryUpdate(x, y, scrollX, scrollY);
this.autoScroll.update(this.scroll, x, y, this.scrollWidth, this.scrollHeight, this.scrollRect);
}
end() {
this.toIx = this.items.indexOf(this[PLACEHOLDER]);
if (this.toIx === -1) {
return; //cancelled
}
this.move(this.toIx < this.fromIx ? this.fromIx + 1 : this.fromIx, this.toIx);
this.drag.end();
this.autoScroll.end();
this.removePlaceholder();
if (this.fromIx < this.toIx) {
--this.toIx;
}
if (this.fromIx !== this.toIx) {
this.moved( { fromIx: this.fromIx, toIx: this.toIx } );
}
}
cancel() {
this.drag.end();
this.autoScroll.end();
this.removePlaceholder();
}
}
@customAttribute("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;
}
table {
margin: 0 auto;
border-collapse: collapse;
}
td div {
box-sizing: border-box;
width: 100px;
border: 1px solid #ccc;
text-align: center;
line-height: 2rem;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment