<html ng-app="taskboard">
<head>
<link href="styles.css" rel="stylesheet">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.9.2/jquery-ui.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.js"></script>
<script src="https://rawgithub.com/angular-ui/ui-sortable/master/src/sortable.js"></script>
</head>
<body ng-controller="TaskController">
<div class="column-wrapper" ng-repeat="column in board.columns">
<div class="column-title">{{ column.name }}</div>
<div ui-multi-sortable ng-model="board" model-subset="columns[{{ $index }}].items" class="column">
<div class="item" ng-repeat="item in column.items">{{ item.name }}</div>
</div>
</div>
<div style="clear: both"></div>
<span>Has underlying model changed? {{changed}}</span>
<div style="clear: both"></div>
<pre ng-bind="board | json"></pre>
<script type="text/javascript">
(function(){
angular.module('ui.sortable').value('uiSortableConfig', {
sortable: {
connectWith: '.column',
update: 'itemsChanged',
}
});
this.taskboard = angular.module('taskboard', ['ui.sortable']);
this.TaskController = function($scope) {
$scope.changed = 'Unfortunately not';
$scope.board = {
columns: [
{ name: 'todo',
items: [
{name: 'todo 1'},
{name: 'todo 2'},
{name: 'todo 3'},
{name: 'todo 4'}
]
},
{ name: 'done',
items: [
{name: 'done 1'},
{name: 'done 2'}
]
},
{ name: 'onhold',
items: []
}
]
};
$scope.itemsChanged = function() {
$scope.changed = 'Yes, of course :)';
}
};
}).call(this);
</script>
</body>
</html>
.column-wrapper {
width: 200px;
float: left;
margin-right: 20px;
}
.column-title {
background-color: #aaa;
font-size: 1.3em;
}
.column {
height: 300px;
background: #ededed;
}
.item {
box-sizing: border-box;
padding: 2px;
background: #8c0;
margin-bottom: 1px;
}
.ui-sortable-helper {
background: #8cf;
}
/**
Angular directive for JQuery UI sortable.
Built on angular-ui "uiSortable" directive.
Adds ability to sort between multiple sortables.
@author: Michal Ostruszka (http://michalostruszka.pl)
**/
angular.module('ui.sortable').directive('uiMultiSortable', ['uiSortableConfig', '$parse', function(uiConfig, $parse) {
var options = {};
if (uiConfig.sortable !== null) {
angular.extend(options, uiConfig.sortable);
}
var ModelSynchronizer = function(uiElement, attrs) {
var MODEL_SUBSET_ATTR = 'ui-sortable-model-subset';
var INITIAL_POSITION_ATTR = 'ui-sortable-start-pos';
var self = this;
// Set some data-* attributes on element being sorted just before sorting starts
this.appendDataOnStart = function() {
uiElement.item.data(INITIAL_POSITION_ATTR, uiElement.item.index());
uiElement.item.data(MODEL_SUBSET_ATTR, attrs.modelSubset);
};
// Update underlying model when elements sorted within one "sortable"
this.updateSingleSortableModel = function(model) {
_collectDataRequiredForModelSync();
if(_isInternalUpdate() && _hasPositionChanged()) {
_update(model);
}
};
// Update underlying model when elements sorted between different "sortables"
this.updateMultiSortableModel = function(model) {
_collectDataRequiredForModelSync();
_update(model);
};
function _collectDataRequiredForModelSync() {
self.data = {
origSubset: uiElement.item.data(MODEL_SUBSET_ATTR),
destSubset: attrs.modelSubset,
origPosition: uiElement.item.data(INITIAL_POSITION_ATTR),
destPosition: uiElement.item.index()
};
}
function _hasPositionChanged() {
return (self.data.origPosition !== self.data.destPosition) || !_isInternalUpdate();
}
function _isInternalUpdate() {
return attrs.modelSubset === undefined || self.data.origSubset === self.data.destSubset;
}
function _update(model) {
if(attrs.modelSubset === undefined) {
model.splice(self.data.destPosition, 0, model.splice(self.data.origPosition, 1)[0]);
} else {
($parse(self.data.destSubset)(model)).splice(self.data.destPosition, 0, ($parse(self.data.origSubset)(model)).splice(self.data.origPosition, 1)[0]);
}
}
};
return {
require: '?ngModel',
link: function(scope, element, attrs, ngModel) {
var opts = angular.extend({}, options, scope.$eval(attrs.uiOptions));
if (ngModel !== null) {
var _start = opts.start;
opts.start = function(e, ui) {
new ModelSynchronizer(ui, attrs).appendDataOnStart();
_callUserDefinedCallback(_start)(e, ui);
return scope.$apply();
};
var _update = opts.update;
opts.update = function(e, ui) {
_callUserDefinedCallback(_update)(e, ui);
return scope.$apply();
};
var _stop = opts.stop;
opts.stop = function(e, ui) {
var modelSync = new ModelSynchronizer(ui, attrs);
modelSync.updateSingleSortableModel(ngModel.$modelValue);
_callUserDefinedCallback(_stop)(e, ui);
return scope.$apply();
};
var _receive = opts.receive;
opts.receive = function(e, ui) {
var modelSync = new ModelSynchronizer(ui, attrs);
modelSync.updateMultiSortableModel(ngModel.$modelValue);
_callUserDefinedCallback(_receive)(e, ui);
return scope.$apply();
};
}
function _callUserDefinedCallback(callback) {
if (typeof callback === "function") {
return callback; // regular callback
}
if(typeof scope[callback] === "function") {
return scope[callback]; // $scope function as callback
}
return function() {}; // noop function
}
return element.sortable(opts);
}
};
}
]);
Output
You can jump to the latest bin by adding /latest
to your URL
Keyboard Shortcuts
Shortcut | Action |
---|---|
ctrl + [num] | Toggle nth panel |
ctrl + 0 | Close focused panel |
ctrl + enter | Re-render output. If console visible: run JS in console |
Ctrl + l | Clear the console |
ctrl + / | Toggle comment on selected lines |
ctrl + ] | Indents selected lines |
ctrl + [ | Unindents selected lines |
tab | Code complete & Emmet expand |
ctrl + shift + L | Beautify code in active panel |
ctrl + s | Save & lock current Bin from further changes |
ctrl + shift + s | Open the share options |
ctrl + y | Archive Bin |
Complete list of JS Bin shortcuts |
JS Bin URLs
URL | Action |
---|---|
/ | Show the full rendered output. This content will update in real time as it's updated from the /edit url. |
/edit | Edit the current bin |
/watch | Follow a Code Casting session |
/embed | Create an embeddable version of the bin |
/latest | Load the very latest bin (/latest goes in place of the revision) |
/[username]/last | View the last edited bin for this user |
/[username]/last/edit | Edit the last edited bin for this user |
/[username]/last/watch | Follow the Code Casting session for the latest bin for this user |
/quiet | Remove analytics and edit button from rendered output |
.js | Load only the JavaScript for a bin |
.css | Load only the CSS for a bin |
Except for username prefixed urls, the url may start with http://jsbin.com/abc and the url fragments can be added to the url to view it differently. |