(Обновлено: см. внизу сообщения) Я новичок в jstree (и JS) и пытаюсь использовать его в сценарии MVC4, где узлы должны динамически загружаться из некоторого репозитория. Мне удалось заставить это работать, но я столкнулся с некоторыми трудностями при определении того, какие узлы были эффективно выбраны пользователем, так как это зависит от статуса пользовательского интерфейса. Начнем с метода контроллера MVC, который возвращает узлы и вызывается JS. Его код выглядит так:
public JsonResult GetTreeNodes(string id)
{
// selectedIds contains some pre-selected IDs...
IEnumerable nodes = _repository.GetChildNodes(id);
return Json(
(from n in nodes
select new
{
id = n.Id,
data = n.Value,
attr = new
{
id = n.Id,
selected = selectedIds.Any(s => s == n.Id)
},
state = "closed"
}).ToArray(), JsonRequestBehavior.AllowGet);
}
Это отлично работает и возвращает дочерние узлы узла с указанным идентификатором в том виде, в каком их ожидает jstree.
В моем представлении есть div для дерева и скрытое поле для включения проверенных узлов. Когда форма отправляется, функция JS извлекает проверенные узлы и сохраняет их в скрытом поле, чтобы действие MVC могло их получить. Вид такой:
...
<section>
@using (Html.BeginForm())
{
@Html.Hidden("selectedNodes")
<div id="tree" style="max-width: 70%"></div>
<input id="submit" type="submit" value="Submit" />
}
</section>
<script type="text/javascript">
$(function () {
fillTree();
$("form:first").submit(function(e) {
storeSelected();
});
});
function fillTree() {
$("#tree").jstree({
checkbox: {
real_checkboxes: true,
checked_parent_open: true
},
plugins: ["themes", "json_data", "ui", "checkbox"],
json_data: {
"ajax": {
"type": 'GET',
"url": function(node) {
var url;
if (node == -1) {
url = "@Url.Action("GetTreeNodes", "Home")";
} else {
var nodeId = node.attr('id');
url = "@Url.Action("GetTreeNodes", "Home")" + "?id=" + nodeId;
}
return url;
},
"success": function (newData) {
return newData;
}
}
},
animation: 200
}).bind("open_node.jstree", function(event, data) {
$("#tree").jstree("check_node", "li[selected=selected]");
});
}
function storeSelected() {
var checkedIds = [];
$("#tree").jstree("get_checked", null, true).each(function() {
checkedIds.push(this.id);
});
$("#selectedNodes").val(checkedIds.join(","));
}
</script>
Как видите, когда пользователь отправляет форму, функция storeSelected использует функцию jstree get_checked для получения проверенных узлов. Проблема в том, что узлы здесь загружаются динамически, поэтому это работает только со ссылкой на узлы, которые фактически присутствуют на странице, когда пользователь отправляет форму. Если я разверну ветвь, проверю и сниму отметку с некоторых узлов, а затем свяжу ту же ветвь перед отправкой, я получу пустой список проверенных идентификаторов только потому, что элементы списка, представляющие узлы в пользовательском интерфейсе дерева, отсутствуют на странице.
Итак, какова наилучшая стратегия для решения этой проблемы? Я думал о том, чтобы отреагировать на событие checked/unchecked и отслеживать действия пользователя, чтобы знать, какие узлы он проверял независимо от состояния пользовательского интерфейса. Поэтому я бы добавил еще одну привязку, подобную этой:
bind("check_node.jstree uncheck_node.jstree", function(event, data) {
var id = data.rslt.obj[0].id;
addToCheckedOrUnchecked(id, event.type == "check_node"); });
и реализовать функцию addToCheckedOrUnchecked, чтобы она могла хранить список идентификаторов, которые пользователи проверяют или снимают. Конечно, при добавлении идентификатора в проверенный список мне придется удалить его из непроверенного списка, если он есть, и наоборот. Я также должен использовать некоторый механизм сохранения для сохранения этих списков между различными вызовами AJAX: я предполагаю, что данные сеанса или файлы cookie, в зависимости от возможностей браузера.
Обратите внимание, что открывать все ветки перед отправкой не вариант, так как это подразумевало бы запрос ветки сервера за веткой, а некоторые из этих веток большие, а я хочу сохранить пропускную способность и отзывчивость.
Моя первая проблема касается событий Check и Uncheck node: они, кажется, не задокументированы (я нашел их погуглить), и кажется, что не все браузеры обрабатывают их правильно. Например, в IE9 событие вообще не срабатывает, а в FF — срабатывает. И, во всяком случае, не могли бы вы предложить более эффективные подходы к этой проблеме? Кажется, это много кода для относительно простой задачи, и поскольку я собираюсь использовать несколько деревьев на разных страницах, мне, вероятно, придется создать помощник HTML, чтобы избежать избыточности в моем проекте MVC.
Обновление №1
Методом проб и ошибок я улучшил код JS, но все еще сталкиваюсь с некоторыми проблемами. Я должен добавить, что мне нужно синхронизировать полученную ветку с еще не отправленными пользовательскими правками: если я открываю ветку, снимаю (предварительно выбранный) элемент и отмечаю другой, затем закрываю ветку и снова открываю ее, это не вызывает новый вызов ajax, поэтому мне нужно обработать событие open_node, чтобы иметь возможность изменять флажки узлов перед их отображением. Во всяком случае, дерево, кажется, игнорирует мой код синхронизации. Прежде всего, функция, используемая для добавления идентификатора узла в список проверенных или непроверенных идентификаторов, поддерживаемых в хранилище сеансов (или в файле cookie):
function addToCheckedOrUnchecked(id, checked) {
// get arrays of IDs from storage
var listChecked = getFromStore("checkedNodes");
if (listChecked === null) {
listChecked = [];
}
var listUnchecked = getFromStore("uncheckedNodes");
if (listUnchecked === null) {
listUnchecked = [];
}
// if checking, add ID to checked and remove from unchecked if present
if (checked) {
if ($.inArray(id, listChecked) == -1) {
listChecked.push(id);
}
var i = $.inArray(id, listUnchecked);
if (i > -1) {
listUnchecked.splice(i, 1);
}
// else add ID to unchecked and remove from checked if present
} else {
if ($.inArray(id, listUnchecked) == -1) {
listUnchecked.push(id);
}
var j = $.inArray(id, listChecked);
if (j > -1) {
listChecked.splice(j, 1);
}
}
// update storage
putInStore("checkedNodes", listChecked);
putInStore("uncheckedNodes", listUnchecked);
}
Эта функция просто считывает из хранилища списки и обновляет их в соответствии с полученным идентификатором и независимо от того, проверяется ли идентификатор или нет.
Затем я добавил эту функцию:
function synchChecks(data) {
var listChecked = getFromStore("checkedNodes");
var listUnchecked = getFromStore("uncheckedNodes");
$(data).each(function () {
var id = $(this).id;
if ($.inArray(id, listChecked) > -1) {
$(this).attr.selected = true;
console.log("overridden=1: " + id);
} else if ($.inArray(id, listUnchecked) > -1) {
$(this).attr.selected = false;
console.log("overridden=0: " + id);
}
});
}
это получает списки из хранилища и переопределяет значение attr.selected каждого полученного узла (в данных), чтобы оно синхронизировалось с изменениями пользователя. Наконец, я склеиваю все вместе в вызове jstree:
function fillTree() {
$("#tree").jstree({
checkbox: {
real_checkboxes: true,
checked_parent_open: true
},
plugins: ["themes", "json_data", "ui", "checkbox"],
json_data: {
"ajax": {
"type": 'GET',
"url": function(node) {
var url;
if (node == -1) {
url = "@Url.Action("GetTreeNodes", "Home")";
} else {
var nodeId = node.attr('id');
url = "@Url.Action("GetTreeNodes", "Home")" + "?id=" + nodeId;
}
return url;
},
"success": function (newData) {
synchChecks(newData);
return newData;
}
}
},
animation: 200
}).bind("open_node.jstree", function (event, data) {
openingNode = true;
var tree = $("#tree");
tree.jstree("check_node", "li[selected=selected]");
// get and synch children
var listChecked = getFromStore("checkedNodes");
var listUnchecked = getFromStore("uncheckedNodes");
$(data.inst._get_children(data.rslt.obj[0])).each(function () {
var id = $(this).attr("id");
if ($.inArray(id, listChecked) > -1) {
$(this).addClass("jstree-checked");
$(this).removeClass("jstree-unchecked");
} else if ($.inArray(id, listUnchecked) > -1) {
$(this).addClass("jstree-unchecked");
$(this).removeClass("jstree-checked");
}
});
openingNode = false;
}).bind("check_node.jstree uncheck_node.jstree", function (event, data) {
if (!openingNode) {
var id = data.rslt.obj[0].id;
addToCheckedOrUnchecked(id, event.type == "check_node");
}
});
}
При успешном вызове ajax я вызываю synchChecks для синхронизации полученных узлов (еще не обновленных, поскольку на сервер ничего не было отправлено) с пользовательскими правками. Кроме того, на open_node я получаю списки проверенных и непроверенных идентификаторов и использую их для переопределения состояния проверки каждого дочернего узла открываемого узла. Наконец, когда пользователь проверяет или снимает отметку с узла, я вызываю addToCheckedOrUnchecked, чтобы обновить свои списки идентификаторов.
В любом случае, похоже, это не работает. Например, если я открою ветку, сниму отметку с предварительно выбранного узла, закрою его и снова открою, отладчик увидит, что идентификаторы списка соответствуют ожидаемым (неотмеченный список имеет идентификатор узла, который я не проверил), и что идентификатор, полученный из li в цикле, в порядке, но мои правки в классах li, похоже, не действуют, и я вижу ветку с проверками, поступающими с сервера. Итак, предполагая, что другого жизнеспособного подхода нет, есть ли у вас какие-либо намеки на то, чтобы позволить этому работать так, как ожидалось?