I'm creating a two-dimensional sortable container with first dimension (rows in table) and second dimension (cells in a row).
The cells should be draggable within a row, to existing rows, to new rows created dynamically. Empty rows should be dynamically deleted. The cells are configured to occupy all space in a row.
How to edit the custom Knockout sortable binding (e.g. update
event)?
Before:
After:
Update problems:
.sortable-cell
)
to a new row (.sortable-table
/.sortable-row
) the viewModel gets updated, but not the UI.highlight-horizontal
) is not displayed, when dragging cell (.sortable-cell
) to a new row (.sortable-table
/.sortable-row
)//connect items with observableArrays
ko.bindingHandlers.sortableList = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
$(element).data("sortList", valueAccessor().data); //attach meta-data
$(element).sortable({
placeholder: valueAccessor().placeholder,
start: function(event, ui) {},
change: function(event, ui) {},
update: function(event, ui) {
var item = ui.item.data("sortItem");
if (item) {
//identify parents
var originalParent = ui.item.data("parentList");
var newParent = ui.item.parent().data("sortList");
//identify viewModel
var viewModel = bindingContext.$root;
//figure out its new position
var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
if (ui.item.parent()[0].classList.contains("sortable-row")) {
//Row already exists
console.log("true");
} else {
//Row doesn't exist, create new row (PROBLEM WITH UPDATE HERE)
newParent().splice(position, 0, {
"children": ko.observableArray([])
});
newParent = newParent()[position].children;
}
//Update item position
originalParent.remove(item);
newParent.splice(position, 0, item);
//Remove empty lists
var children = viewModel.children();
for (var i = 0; i < children.length; i++) {
if (children[i].children().length == 0) {
viewModel.children.remove(children[i]);
console.log(children);
}
}
//Update UI
ui.item.remove();
//Debug data model
console.log("final viewModel");
var children = viewModel.children();
for (var i = 0; i < children.length; i++) {
console.log(children[i].children());
for (var j = 0; j < children[i].children().length; j++) {
console.log(children[i].children()[j].children(),children[i].children()[j].content());
}
}
}
},
connectWith: '.sortable-container'
});
}
};
//attach meta-data
ko.bindingHandlers.sortableItem = {
init: function(element, valueAccessor) {
var options = valueAccessor();
$(element).data("sortItem", options.item);
$(element).data("parentList", options.parentList);
}
};
var self = this;
var viewModel = function() {
var self = this;
self.children = ko.observableArray(
[{
"children": ko.observableArray([{
"content": ko.observable("Item 1"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 2"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 3"),
"children": ko.observableArray([])
}])
}, {
"children": ko.observableArray([{
"content": ko.observable("Item 4"),
"children": ko.observableArray([])
}])
}, {
"children": ko.observableArray([{
"content": ko.observable("Item 5"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 6"),
"children": ko.observableArray([])
}])
}]
);
};
ko.applyBindings(new viewModel());
.sortable-table {
border: 1px red solid;
padding: 10px 0px;
list-style-type: none;
width: 100% !important;
display: table !important;
}
.sortable-table .sortable-row {
height: 100% !important;
display: table-row !important;
padding: 5px 0px;
}
.sortable-table .sortable-cell {
border: 1px solid green;
display: table-cell !important;
cursor: move;
}
.sortable-table .sortable-cell p {
display: inline;
margin: 0 !important;
}
.sortable-table .highlight-vertical {
width: 5px !important;
display: table-cell !important;
background-color: blue !important;
}
.sortable-table .highlight-horizontal {
height: 5px !important;
width: 100% !important;
display: block !important;
background-color: blue !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://rniemeyer.github.com/KnockMeOut/Scripts/jquery.tmpl.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<div class="sortable-container" data-bind="template: { name: 'rowTmpl', foreach: $data.children, templateOptions: { parentList: $data.children } }, sortableList: { data: $data.children, placeholder: 'highlight-horizontal' }">
</div>
<script id="rowTmpl" type="text/html">
<div class="sortable-table">
<div class="sortable-row sortable-container" data-bind="template: { name: 'cellTmpl', foreach: $data.children, templateOptions: { parentList: $data.children } }, sortableList: { data: $data.children, placeholder: 'highlight-vertical' }">
</div>
</div>
</script>
<script id="cellTmpl" type="text/html">
<div class="sortable-cell" data-bind="sortableItem: { item: $data, parentList: $item.parentList }">
<p data-bind="text: $data.content"></p>
</div>
</script>
The problem was on line newParent.splice(position, 0, {"children": ko.observableArray([])});
. newParent
was called as newParent()
, which was causing the problem.
//connect items with observableArrays
ko.bindingHandlers.sortableList = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
$(element).data("sortList", valueAccessor().data); //attach meta-data
$(element).sortable({
placeholder: valueAccessor().placeholder,
start: function(event, ui) {},
change: function(event, ui) {},
update: function(event, ui) {
var item = ui.item.data("sortItem");
if (item) {
//identify parents
var originalParent = ui.item.data("parentList");
var newParent = ui.item.parent().data("sortList");
//identify viewModel
var viewModel = bindingContext.$root;
//figure out its new position
var position = ko.utils.arrayIndexOf(ui.item.parent().children(), ui.item[0]);
if (ui.item.parent()[0].classList.contains("sortable-row")) {
//Row already exists
console.log("true");
} else {
//Row doesn't exist, create new row (PROBLEM WITH UPDATE HERE)
newParent.splice(position, 0, {
"children": ko.observableArray([])
});
newParent = newParent()[position].children;
}
//Update item position
originalParent.remove(item);
newParent.splice(position, 0, item);
//Remove empty lists
var children = viewModel.children();
for (var i = 0; i < children.length; i++) {
if (children[i].children().length == 0) {
viewModel.children.remove(children[i]);
console.log(children);
}
}
//Update UI
ui.item.remove();
//Debug data model
console.log("final viewModel");
var children = viewModel.children();
for (var i = 0; i < children.length; i++) {
console.log(children[i].children());
for (var j = 0; j < children[i].children().length; j++) {
console.log(children[i].children()[j].children(),children[i].children()[j].content());
}
}
}
},
connectWith: '.sortable-container'
});
}
};
//attach meta-data
ko.bindingHandlers.sortableItem = {
init: function(element, valueAccessor) {
var options = valueAccessor();
$(element).data("sortItem", options.item);
$(element).data("parentList", options.parentList);
}
};
var self = this;
var viewModel = function() {
var self = this;
self.children = ko.observableArray(
[{
"children": ko.observableArray([{
"content": ko.observable("Item 1"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 2"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 3"),
"children": ko.observableArray([])
}])
}, {
"children": ko.observableArray([{
"content": ko.observable("Item 4"),
"children": ko.observableArray([])
}])
}, {
"children": ko.observableArray([{
"content": ko.observable("Item 5"),
"children": ko.observableArray([])
}, {
"content": ko.observable("Item 6"),
"children": ko.observableArray([])
}])
}]
);
};
ko.applyBindings(new viewModel());
.sortable-table {
border: 1px red solid;
padding: 10px 0px;
list-style-type: none;
width: 100% !important;
display: table !important;
}
.sortable-table .sortable-row {
height: 100% !important;
display: table-row !important;
padding: 5px 0px;
}
.sortable-table .sortable-cell {
border: 1px solid green;
display: table-cell !important;
cursor: move;
}
.sortable-table .sortable-cell p {
display: inline;
margin: 0 !important;
}
.sortable-table .highlight-vertical {
width: 5px !important;
display: table-cell !important;
background-color: blue !important;
}
.sortable-table .highlight-horizontal {
height: 5px !important;
width: 100% !important;
display: block !important;
background-color: blue !important;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://rniemeyer.github.com/KnockMeOut/Scripts/jquery.tmpl.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/2.2.1/knockout-min.js"></script>
<div class="sortable-container" data-bind="template: { name: 'rowTmpl', foreach: $data.children, templateOptions: { parentList: $data.children } }, sortableList: { data: $data.children, placeholder: 'highlight-horizontal' }">
</div>
<script id="rowTmpl" type="text/html">
<div class="sortable-table">
<div class="sortable-row sortable-container" data-bind="template: { name: 'cellTmpl', foreach: $data.children, templateOptions: { parentList: $data.children } }, sortableList: { data: $data.children, placeholder: 'highlight-vertical' }">
</div>
</div>
</script>
<script id="cellTmpl" type="text/html">
<div class="sortable-cell" data-bind="sortableItem: { item: $data, parentList: $item.parentList }">
<p data-bind="text: $data.content"></p>
</div>
</script>