I am working on a page that is using JQuery mobile expandable list
and Knockout.js
combination. I have written mechanism that allows to build ViewModel tree structure and bind it to expandable list. Since some of the treenodes could be large I have used koncokout :if
bindings so that nodes would not get rendered until they are being expanded (on first page load full model is built but only 20 records rendered). At one point I ran into problem of parent node being closed when children of children are still open and model going out of synch (on reopening parent node children of children would be hidden as according to model they have never been closed). So I have used MutationObservable
to handle these situations if node is being 'vaporised' from DOM
tree to set model for all children of that node to closed.
function ShowChildren(data, element) {
if (data.IsExpanded != undefined) {
data.IsExpanded(!data.IsExpanded());
if (data.IsExpanded()) {
var childContentElements = $(element).parent().find('.ui-collapsible-content');
childContentElements.attr('data-bind','visible : $parent.IsExpanded != undefined && $parent.IsExpanded()');
AttachVisibilityChangedEventHandler({ targets: childContentElements, eventHandler: VisibilityChangedEventHandler });
$.each(childContentElements, function (i, item) { item.addEventListener('DOMNodeRemovedFromDocument', TreeNodeClosedEventHandler); });
$("[data-role='none']").attr('data-role','collapsible');
$("[data-role='collapsible-set'][data-onthemovedatasourceid='bcAccount']").trigger('create');
OnCollapsibleSetCreateJQueryMobileUI(element);
$.each($.grep($(element).parent().parent().find(">[data-role='collapsible']").find(">.ui-collapsible-content:not(.ui-collapsible-content-collapsed)").siblings(), function (item) { return element != item; }), function (i, item) { $(item).find('>a').click(); });
}
}
}
function OnCollapsibleSetCreateJQueryMobileUI(element) {
$(element).parent().parent().parent().parent().find('>h3>a').removeClass('ui-corner-bottom');
$(element).parent().find('>.ui-collapsible-content').removeClass('ui-corner-bottom');
}
function ColapseAllSiblings(parent, id) {
if (parent.Children != undefined) {
for (var i = 0; i < parent.Children.length; i++) {
if (parent.Children[i]['Id'] != id) {
if (parent.Children[i].IsExpanded != undefined && parent.Children[i].IsExpanded()) {
parent.Children[i].IsExpanded(false);
ColapseAllChildren(parent.Children[i]);
}
}
}
}
}
function ColapseAllChildren(data) {
if (data.Children != undefined) {
for (var i = 0; i < data.Children.length; i++) {
if (data.Children[i].IsExpanded != undefined && data.Children[i].IsExpanded()) {
data.Children[i].IsExpanded(false);
ColapseAllChildren(data.Children[i]);
}
}
}
}
function AttachVisibilityChangedEventHandler(options) {
var targets = options.targets ? options.targets : $(options.selector);
$.each(targets, function (i, target) {
target.addEventListener('visibilityChanged', options.eventHandler, false);
PageObj.VisibilityObserver.observe(target, { attributes: true, attributeFilter: ['class'], childList: false, attributeOldValue: true });
});
}
function VisibilityChangedEventHandler() {
var knockoutContext = ko.contextFor(this);
if (!$(this).is(':visible')){
if (knockoutContext.$parents.length > 2) {
ColapseAllChildren(knockoutContext.$data);
} else {
ColapseAllChildren(knockoutContext.$parents[0]);
}
} else {
if (knockoutContext.$parents.length > 2) {
var recordId = typeof knockoutContext.$data.Id == 'function' ? knockoutContext.$data['Id']() : knockoutContext.$data['Id'];
if (knockoutContext.$parents.length > 3) {
ColapseAllSiblings(knockoutContext.$parents[0], recordId);
} else {
ColapseAllSiblings(knockoutContext.$parents[1], recordId);
}
}
}
}
$(onTheMove.PageDataRoles).on('OnRender', function () {
$('.AppletBase >.ui-collapsible-content').attr('data-bind','visible : $parent.IsExpanded != undefined && $parent.IsExpanded()');
PageObj.VisibilityObserver = new MutationObserver(function (mutations) {
var clone = $(mutations[0].target).clone();
clone.removeClass();
for (var i = 0; i < mutations.length; i++) {
clone.addClass(mutations[i].oldValue);
}
$(document.body).append(clone);
var cloneVisibility = $(clone).is(":visible");
$(clone).remove();
if (cloneVisibility != $(mutations[0].target).is(":visible")) {
var visibilityChangedEvent = document.createEvent('Event');
visibilityChangedEvent.initEvent('visibilityChanged', false, true);
mutations[0].target.dispatchEvent(visibilityChangedEvent);
}
});
AttachVisibilityChangedEventHandler({ selector: '.ui-collapsible-content', eventHandler: VisibilityChangedEventHandler });
});
function TreeNodeClosedEventHandler(e) {
ColapseAllChildren(ko.contextFor(e.target).$data);
}
It all worked fine with all the latest versions of all major browsers so project went ahead with this mechanism in place. Until we picked up Samsung tabled with Android Browser 4
that does not support MutationObservable
, which is a 'must' for one of our clients.
What substitute mechanism could I put in place of MutationObserver
so I wouldn't need to rewrite the whole thing? Or any other solution that does not require full rewrite.