forked from j62/ctbrec
Make model table sortable
This commit is contained in:
parent
2131b596cb
commit
d6beaac5f9
|
@ -34,4 +34,30 @@
|
|||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.asc:after, .desc:after {
|
||||
font-family: 'FontAwesome';
|
||||
padding-left: .5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.asc:after {
|
||||
content: "\f0de"; /* unicode sort up */
|
||||
}
|
||||
|
||||
.desc:after {
|
||||
content: "\f0dd"; /* unicode sort down */
|
||||
}
|
||||
|
||||
th a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
th a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@
|
|||
<div id="alert-container"></div>
|
||||
|
||||
<div class="tab-content" id="myTabContent">
|
||||
<section id="models" class="tab-pane fade" role="tabpanel" aria-labelledby="models-tab">
|
||||
<section id="models" class="tab-pane fade show active" role="tabpanel" aria-labelledby="models-tab">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 mx-auto">
|
||||
|
@ -116,19 +116,19 @@
|
|||
<table class="table table-bordered table-hover table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Model</th>
|
||||
<th>Paused</th>
|
||||
<th>Online</th>
|
||||
<th>Recording</th>
|
||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_name'}">Model</a></th>
|
||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_suspended'}">Paused</a></th>
|
||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_online'}">Online</a></th>
|
||||
<th><a href="#" data-bind="orderable: {collection: 'models', field: 'ko_recording'}">Recording</a></th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody data-bind="foreach: models">
|
||||
<tr>
|
||||
<td><a data-bind="attr: { href: url, title: name }, text: name"></a></td>
|
||||
<td><input type="checkbox" data-bind="checked: suspended" /></td>
|
||||
<td><input type="checkbox" disabled data-bind="checked: online" /></td>
|
||||
<td><input type="checkbox" disabled data-bind="checked: recording" /></td>
|
||||
<td><a data-bind="attr: { href: ko_url, title: ko_name }, text: ko_name"></a></td>
|
||||
<td><input type="checkbox" data-bind="checked: ko_suspended" /></td>
|
||||
<td><input type="checkbox" disabled data-bind="checked: ko_online" /></td>
|
||||
<td><input type="checkbox" disabled data-bind="checked: ko_recording" /></td>
|
||||
<td><button class="btn btn-secondary fa fa-minus-circle" title="Stop recording" data-bind="click: ctbrec.stop"></button></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -138,7 +138,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section id="recordings" class="tab-pane fade show active" role="tabpanel" aria-labelledby="recordings-tab">
|
||||
<section id="recordings" class="tab-pane fade" role="tabpanel" aria-labelledby="recordings-tab">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-10 mx-auto">
|
||||
|
@ -248,6 +248,7 @@
|
|||
|
||||
<!-- knockout -->
|
||||
<script src="/static/vendor/knockout/knockout-3.5.0.js"></script>
|
||||
<script src="/static/vendor/knockout-orderable/knockout.bindings.orderable.js"></script>
|
||||
|
||||
|
||||
<!-- Custom scripts for this template -->
|
||||
|
@ -459,6 +460,89 @@
|
|||
}
|
||||
setTimeout(updateOnlineModels, 3000);
|
||||
}
|
||||
|
||||
function isModelInArray(array, model) {
|
||||
for (let idx in array) {
|
||||
let m = array[idx];
|
||||
if(m.url === model.url) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes models from the server with the displayed
|
||||
* knockout model table
|
||||
*/
|
||||
function syncModels(models) {
|
||||
// remove models from the observable array, which are not in the
|
||||
// updated list
|
||||
for (let idx in observableModelsArray()) {
|
||||
let model = observableModelsArray()[idx];
|
||||
if(!isModelInArray(models, model)) {
|
||||
console.log('remove', model);
|
||||
observableModelsArray.remove(model);
|
||||
}
|
||||
}
|
||||
|
||||
// add models to the observable array, which are new in the
|
||||
// updated list
|
||||
for (let idx in models) {
|
||||
let model = models[idx];
|
||||
if (!isModelInArray(observableModelsArray(), model)) {
|
||||
model.ko_name = ko.observable(model.name);
|
||||
model.ko_url = ko.observable(model.url);
|
||||
model.ko_online = ko.observable(false);
|
||||
for ( let i in onlineModels) {
|
||||
let onlineModel = onlineModels[i];
|
||||
if (onlineModel.url === model.url) {
|
||||
model.ko_online(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
model.ko_recording = ko.observable(model.online && !model.suspended);
|
||||
model.ko_suspended = ko.observable(model.suspended);
|
||||
model.swallowEvents = false;
|
||||
model.ko_suspended.subscribe(function(checked) {
|
||||
if (model.swallowEvents) {
|
||||
return;
|
||||
}
|
||||
if (!checked) {
|
||||
ctbrec.resume(model);
|
||||
} else {
|
||||
ctbrec.suspend(model);
|
||||
}
|
||||
});
|
||||
observableModelsArray.push(model);
|
||||
}
|
||||
}
|
||||
|
||||
// update existing models
|
||||
for (let i in models) {
|
||||
let model = models[i];
|
||||
for (let j in observableModelsArray()) {
|
||||
let m = observableModelsArray()[j];
|
||||
if(model.url === m.ko_url()) {
|
||||
m.ko_name(model.name);
|
||||
m.ko_url(model.url);
|
||||
let onlineState = false;
|
||||
for ( let i in onlineModels) {
|
||||
let onlineModel = onlineModels[i];
|
||||
if (onlineModel.url === model.url) {
|
||||
onlineState = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m.ko_online(onlineState);
|
||||
m.swallowEvents = true;
|
||||
m.ko_suspended(model.suspended);
|
||||
m.swallowEvents = false;
|
||||
m.ko_recording(m.ko_online() && !m.ko_suspended());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateModels() {
|
||||
try {
|
||||
|
@ -471,27 +555,7 @@
|
|||
data : '{"action": "list"}',
|
||||
success : function(data) {
|
||||
if (data.status === 'success') {
|
||||
observableModelsArray.removeAll();
|
||||
for ( let idx in data.models) {
|
||||
let model = data.models[idx];
|
||||
model.online = false
|
||||
for ( let i in onlineModels) {
|
||||
let onlineModel = onlineModels[i];
|
||||
if (onlineModel.url === model.url) {
|
||||
model.online = true;
|
||||
}
|
||||
}
|
||||
model.recording = model.online && !model.suspended;
|
||||
model.suspended = ko.observable(model.suspended);
|
||||
model.suspended.subscribe(function(checked) {
|
||||
if(!checked) {
|
||||
ctbrec.resume(model);
|
||||
} else {
|
||||
ctbrec.suspend(model);
|
||||
}
|
||||
});
|
||||
observableModelsArray.push(model);
|
||||
}
|
||||
syncModels(data.models);
|
||||
} else {
|
||||
console.log('request failed', data);
|
||||
}
|
||||
|
@ -543,7 +607,7 @@
|
|||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
setTimeout(updateRecordings, 3000);
|
||||
setTimeout(updateRecordings, 60000);
|
||||
}
|
||||
|
||||
updateOnlineModels();
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
Knockout orderable binding
|
||||
==================
|
||||
This knockout binding allows to create sortable tables. It works by sorting items in observableArray, so tables rendered by "foreach" binding are automatically refreshed by knockout.
|
||||
|
||||
##Design Goals
|
||||
|
||||
###Focused only on ordering
|
||||
Some other plugins take control of entire table rendering, making it complicated to build arbitrary tables. This plugin is only focused on sorting of observableArrays, leaving rendering of HTML to knockout. It gives freedom of using custom item templates for tables or lists.
|
||||
|
||||
###Controllable from a view model
|
||||
When binding applied for the first time it adds extra observables to a view model. So order can be changed programmatically like
|
||||
|
||||
viewModel.people.orderDirection("desc")
|
||||
or
|
||||
|
||||
viewModel.people.orderDirection("lastName")
|
||||
|
||||
###Minimal view model setup
|
||||
There is nothing to be configured on a view model manually to use the plugin. Only bindings have to be set on elements which will trigger observableArray to be reordered.
|
||||
|
||||
###Can be used with multiple observableArrays
|
||||
Works well with multiple observableArray. oservableArrays can be ordered independently of each other.
|
||||
|
||||
|
||||
##Usage
|
||||
To make table header sortable set binding like this:
|
||||
|
||||
<th><a href="#" data-bind="orderable: {collection: 'people', field: 'firstName'}">First Name</a></th>
|
||||
|
||||
Default field to sort can also be provided:
|
||||
|
||||
<th><a href="#" data-bind="orderable: {collection: 'people', field: 'age', defaultField: true, defaultDirection: 'desc'}">Age</a></th>
|
||||
|
||||
It's also possible to sort by nested attibutes by separating the attribute names with a dot (should work with array indices too):
|
||||
|
||||
<th><a href="#" data-bind="orderable: {collection: 'people', field: 'pet.name'}">Pet name</a></th>
|
||||
|
||||
See full examples in examples folder.
|
||||
|
||||
##Dependencies
|
||||
- [jQuery](http://jquery.com/)
|
||||
- [Knockout](http://knockoutjs.com/)
|
||||
|
||||
##License
|
||||
MIT license - [http://www.opensource.org/licenses/mit-license.php](http://www.opensource.org/licenses/mit-license.php)
|
112
server/src/main/resources/html/static/vendor/knockout-orderable/knockout.bindings.orderable.js
vendored
Normal file
112
server/src/main/resources/html/static/vendor/knockout-orderable/knockout.bindings.orderable.js
vendored
Normal file
|
@ -0,0 +1,112 @@
|
|||
ko.bindingHandlers.orderable = {
|
||||
getProperty: function(o, s) {
|
||||
// copied from http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key
|
||||
s = s.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
|
||||
s = s.replace(/^\./, ''); // strip a leading dot
|
||||
var a = s.split('.');
|
||||
while (a.length) {
|
||||
var n = a.shift();
|
||||
if (n in o) {
|
||||
o = o[n];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return o;
|
||||
},
|
||||
|
||||
compare: function (left, right) {
|
||||
if (typeof left === 'string' || typeof right === 'string') {
|
||||
return left ? left.localeCompare(right) : 1;
|
||||
}
|
||||
if (left > right)
|
||||
return 1;
|
||||
|
||||
return left < right ? -1 : 0;
|
||||
},
|
||||
|
||||
sort: function (viewModel, collection, field) {
|
||||
//make sure we sort only once and not for every binding set on table header
|
||||
if (viewModel[collection].orderField() == field) {
|
||||
viewModel[collection].sort(function (left, right) {
|
||||
var left_field = ko.bindingHandlers.orderable.getProperty(left, field);
|
||||
var right_field = ko.bindingHandlers.orderable.getProperty(right, field);
|
||||
var left_val = (typeof left_field === 'function') ? left_field() : left_field;
|
||||
right_val = (typeof right_field === 'function') ? right_field() : right_field;
|
||||
if (viewModel[collection].orderDirection() == "desc") {
|
||||
return ko.bindingHandlers.orderable.compare(right_val, left_val);
|
||||
} else {
|
||||
return ko.bindingHandlers.orderable.compare(left_val, right_val);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||
//get provided options
|
||||
var collection = valueAccessor().collection;
|
||||
var field = valueAccessor().field;
|
||||
|
||||
//add a few observables to ViewModel to track order field and direction
|
||||
if (viewModel[collection].orderField == undefined) {
|
||||
viewModel[collection].orderField = ko.observable();
|
||||
}
|
||||
if (viewModel[collection].orderDirection == undefined) {
|
||||
viewModel[collection].orderDirection = ko.observable("asc");
|
||||
}
|
||||
|
||||
var defaultField = valueAccessor().defaultField;
|
||||
var defaultDirection = valueAccessor().defaultDirection || "asc";
|
||||
if (defaultField) {
|
||||
viewModel[collection].orderField(field);
|
||||
viewModel[collection].orderDirection(defaultDirection);
|
||||
ko.bindingHandlers.orderable.sort(viewModel, collection, field);
|
||||
}
|
||||
|
||||
//set order observables on table header click
|
||||
$(element).click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
//flip sort direction if current sort field is clicked again
|
||||
if (viewModel[collection].orderField() == field) {
|
||||
if (viewModel[collection].orderDirection() == "asc") {
|
||||
viewModel[collection].orderDirection("desc");
|
||||
} else {
|
||||
viewModel[collection].orderDirection("asc");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel[collection].orderField(field);
|
||||
});
|
||||
|
||||
//order records when observables changes, so ordering can be changed programmatically
|
||||
viewModel[collection].orderField.subscribe(function () {
|
||||
ko.bindingHandlers.orderable.sort(viewModel, collection, field);
|
||||
});
|
||||
viewModel[collection].orderDirection.subscribe(function () {
|
||||
ko.bindingHandlers.orderable.sort(viewModel, collection, field);
|
||||
});
|
||||
},
|
||||
|
||||
update: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {
|
||||
//get provided options
|
||||
var collection = valueAccessor().collection;
|
||||
var field = valueAccessor().field;
|
||||
var isOrderedByThisField = viewModel[collection].orderField() == field;
|
||||
|
||||
//apply css binding programmatically
|
||||
ko.bindingHandlers.css.update(
|
||||
element,
|
||||
function () {
|
||||
return {
|
||||
sorted: isOrderedByThisField,
|
||||
asc: isOrderedByThisField && viewModel[collection].orderDirection() == "asc",
|
||||
desc: isOrderedByThisField && viewModel[collection].orderDirection() == "desc"
|
||||
};
|
||||
},
|
||||
allBindingsAccessor,
|
||||
viewModel,
|
||||
bindingContext
|
||||
);
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue