Skip to content

Instantly share code, notes, and snippets.

@hfitzwater
Forked from jdanyow/app.html
Last active January 31, 2017 15:46
Show Gist options
  • Save hfitzwater/1361422324a7cd5f96c5f14a083d9c1c to your computer and use it in GitHub Desktop.
Save hfitzwater/1361422324a7cd5f96c5f14a083d9c1c to your computer and use it in GitHub Desktop.
Nested drag and drop
<template>
<require from="tree-renderer"></require>
<h1>
Nested drag and drop
</h1>
<tree-renderer
node.bind="tree"
node-view-path="data-item-view.html"
node-model-path="data-item-view.js">
</tree-renderer>
</template>
import {DataItem} from 'data-item.js';
import {TreeNode} from 'tree-node.js';
export class App {
tree = new TreeNode( new DataItem('root'), true );
constructor() {
let one = new TreeNode( new DataItem('one') );
let oneA = new TreeNode( new DataItem('A') );
let oneAone = new TreeNode( new DataItem('one A one') );
oneA.addChildren( [oneAone] );
let oneB = new TreeNode( new DataItem('B') );
let oneC = new TreeNode( new DataItem('C') );
one.addChildren( [oneA, oneB, oneC] );
let two = new TreeNode( new DataItem('two') );
let twoA = new TreeNode( new DataItem('A') );
let twoB = new TreeNode( new DataItem('B') );
let twoC = new TreeNode( new DataItem('C') );
two.addChildren( [twoA, twoB, twoC] );
this.tree.addChildren( [one, two] );
}
}
<template>
<div style="padding:4px; border: 1px solid #ccc; background-color: #eee; width: 150px; cursor: move;">
${ node.data.name } - ${ node.index }
<button style="float:right;" click.delegate="toggleChildren()"> v </button>
</div>
</template>
export class DataItemView {
constructor() {
}
activate( model ) {
this.node = model;
}
toggleChildren() {
this.node.showChildren = !this.node.showChildren;
}
}
export class DataItem {
name = null;
constructor( name ) {
this.name = name;
}
}
<!doctype html>
<html>
<head>
<title>Aurelia</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
.gu-mirror{position:fixed!important;margin:0!important;z-index:9999!important;opacity:.8;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=80)";filter:alpha(opacity=80)}.gu-hide{display:none!important}.gu-unselectable{-webkit-user-select:none!important;-moz-user-select:none!important;-ms-user-select:none!important;user-select:none!important}.gu-transit{opacity:.2;-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=20)";filter:alpha(opacity=20)}
</style>
</head>
<body aurelia-app>
<h1>Loading...</h1>
<script src="https://cdn.rawgit.com/jdanyow/aurelia-bundle/v1.0.3/jspm_packages/system.js"></script>
<script src="https://cdn.rawgit.com/jdanyow/aurelia-bundle/v1.0.3/config.js"></script>
<script src="https://cdn.rawgit.com/bevacqua/dragula/master/dist/dragula.js"></script>
<script>
System.import('aurelia-bootstrapper');
</script>
</body>
</html>
<template>
<div>
<div>
<compose
view.bind="nodeViewPath"
view-model.bind="nodeModelPath"
model.bind="node">
</compose>
</div>
</div>
<div show.bind="node.showChildren" style="position:relative; left: 15px; min-height: 15px;" class="dropzone">
<div repeat.for="node of node.children" class="draggable">
<!--
model.bind get placed on the element
that way we can go from HTMLElement to Aurelia ViewModel
http://aurelia.io/hub.html#/doc/article/aurelia/templating/latest/templating-basics/4
-->
<node-renderer
node.bind="node"
model.bind="node"
id="${node.id}"
node-view-path.bind="nodeViewPath"
node-model-path.bind="nodeModelPath"
drake.bind="drake">
</node-renderer>
</div>
</div>
</template>
import {bindable} from 'aurelia-framework';
export class NodeRenderer {
@bindable node;
@bindable nodeViewPath;
@bindable nodeModelPath;
@bindable drake;
constructor() {
}
}
const NODE_EVENTS = {
ADD: 'tree-node-add',
REMOVE: 'tree-node-remove'
};
export class TreeNode {
children = [];
data = null;
isRoot = false;
id = null;
parent = null;
index = 0;
showChildren = true;
constructor( data, isRoot ) {
this.data = data;
this.isRoot = isRoot;
this.id = this.getGuid();
}
static get EVENTS() {
return NODE_EVENTS;
}
addChild( childNode ) {
this.addChildren( [childNode] );
}
addChildren( children ) {
let length = this.children.length;
children.forEach( (child, index) => {
child.index = length + index;
child.parent = this;
this.children.push( child );
});
}
move( item, before, from, to ) {
let node = item;
let index = to.children.indexOf( before );
let beforeString = '';
try {
beforeString = before.data.name;
} catch( ex ) {
beforeString = 'the end';
}
this.reIndexChildren( to );
if( from !== to ) {
this.reIndexChildren( from );
}
}
reIndexChildren( to, from ) {
let dropzone = this.getStageDropzoneElement( to );
let elements = [].slice.call(dropzone.children);
elements.forEach( (element, index) => {
let child = this.getChildById( element.querySelector('node-renderer').id );
if( child ) {
child.index = index;
}
});
}
getChildById( id ) {
let root = this;
while( root.parent ) {
root = root.parent;
}
let child = this.getChildFromNode( root, id );
if( child ) {
return child;
} else {
console.error('could not find ' + id);
}
}
getChildFromNode( node, id ) {
let all = [node];
this.gatherChildren( node, all );
let found = all.find( child => {
return child.id === id;
});
return found;
}
gatherChildren( node, all ) {
if( !node.children || node.children.length === 0 ) return;
node.children.forEach( child => {
all.push( child );
this.gatherChildren( child, all );
});
}
getStageDropzoneElement( node ) {
let nodeRenderer = document.getElementById( node.id );
let dropzone = nodeRenderer.querySelector('.dropzone');
return dropzone;
}
getGuid() {
/*
* http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
*/
let s4 = () => {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
};
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
}
<template>
<require from="node-renderer"></require>
<label>
Events
</label>
<div style="color:#fff; background-color:#222; padding:5px;max-height: 120px; height:120px; overflow-y:scroll;">
<div repeat.for="event of loggedEvents">
${ event }
</div>
</div>
<br>
<br>
<div>
<div>
<node-renderer
node.bind="node"
id="${node.id}"
node-view-path.bind="nodeViewPath"
node-model-path.bind="nodeModelPath"
drake.bind="drake">
</node-renderer>
</div>
</div>
</template>
import {customElement, bindable, inject, TaskQueue} from 'aurelia-framework';
import {EventAggregator} from 'aurelia-event-aggregator';
const EVENTS = {
ADD: 'tree-node-add',
REMOVE: 'tree-node-remove'
}
@customElement( 'tree-renderer' )
@inject( EventAggregator, TaskQueue )
export class TreeRenderer {
@bindable node;
@bindable nodeViewPath;
@bindable nodeModelPath;
constructor( events, taskQueue ) {
this.events = events;
this.taskQueue = taskQueue;
this.loggedEvents = [];
this.initDrag();
}
attached() {
let addListener = this.events.subscribe( EVENTS.ADD, (event) => {
let beforeText = '';
try {
beforeText = event.before.data.name;
} catch( ex ) {
beforeText = 'the end';
}
this.logEvent(`${event.item.data.name} moved to ${event.to.data.name} before ${beforeText}`);
});
let removeListener = this.events.subscribe( EVENTS.REMOVE, (event) => {
this.logEvent(`${event.item.data.name} removed from ${event.from.data.name}`);
});
this.listeners = [ addListener, removeListener ];
}
logEvent( text ) {
this.loggedEvents.push( text );
console.log( text );
}
detached() {
this.listeners.forEach( listener => {
listener.dispose();
});
}
initDrag() {
this.drake = dragula({
isContainer: (el) => {
return el.classList.contains('dropzone');
},
moves: (el) => {
return el.classList.contains('draggable');
}
});
this.drake.on( 'drop', (el, target, source, sibling) => {
el = el.querySelector('node-renderer');
let moved = el.model;
let newParent = this.getNewParent( el );
let oldParent = this.getOldParent( el );
let before = null;
if( sibling ) {
before = sibling.querySelector('node-renderer').model;
}
this.taskQueue.queueTask(() => {
this.node.move( moved, before, oldParent, newParent );
let addEvent = {
item: moved,
to: newParent,
before: before
};
let removeEvent = {
item: moved,
from: oldParent
};
this.events.publish( EVENTS.ADD, addEvent );
if( newParent !== oldParent ) {
this.events.publish( EVENTS.REMOVE, removeEvent );
}
});
});
}
getOldParent( element ) {
return element.model.parent;
}
getNewParent( element ) {
let maxDistance = 20;
let el = element.parentElement;
for( let i=0; i<maxDistance; i++ ) {
if( el.tagName.toLowerCase() == 'node-renderer' ) {
break;
}
el = el.parentElement;
}
return el.model || this.node;
}
}
import {TreeNode} from 'tree-node.js';
export class Tree {
root = null;
constructor( rootNode ) {
this.root = rootNode;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment