silverstripe-framework/javascript/TreeDropdownField.js
Mateusz Uzdowski c785f3c492 BUG Adjust the tree construction and triggers to work with IE.
After Hamish's suggestion. Entwine onchange would not get executed in
IE8 at all, which would have the effect of the displyed dropdown
selection not being set.

Change to onadd also mandates the changes to onadd on other parts of the
tree component - otherwise the change event can trigger before the
tree elements are added to the DOM.
2012-07-12 16:44:21 +12:00

380 lines
11 KiB
JavaScript

(function($) {
$.entwine('ss', function($){
/**
* On resize of any close the open treedropdownfields
* as we'll need to redo with widths
*/
$(window).resize(function() {
$('.TreeDropdownField').closePanel();
});
var strings = {
'openlink': 'Open',
'fieldTitle': '(choose)',
'searchFieldTitle': '(choose or search)'
};
var _clickTestFn = function(e) {
// If the click target is not a child of the current field, close the panel automatically.
if(!$(e.target).parents('.TreeDropdownField').length) $('.TreeDropdownField').closePanel();
};
/**
* @todo Error display
* @todo No results display for search
* @todo Automatic expansion of ajax children when multiselect is triggered
* @todo Automatic panel positioning based on available space (top/bottom)
* @todo forceValue
* @todo Automatic width
* @todo Expand title height to fit all elements
*/
$('.TreeDropdownField').entwine({
onadd: function() {
this.append(
'<span class="treedropdownfield-title"></span>' +
'<div class="treedropdownfield-toggle-panel-link"><a href="#" class="ui-icon ui-icon-triangle-1-s"></a></div>' +
'<div class="treedropdownfield-panel"><div class="tree-holder"></div></div>'
);
var linkTitle = strings.openLink;
if(linkTitle) this.find("treedropdownfield-toggle-panel-link a").attr('title', linkTitle);
if(this.data('title')) this.setTitle(decodeURIComponent(this.data('title')));
this.getPanel().hide();
this._super();
},
getPanel: function() {
return this.find('.treedropdownfield-panel');
},
openPanel: function() {
// close all other panels
$('.TreeDropdownField').closePanel();
// Listen for clicks outside of the field to auto-close it
$('body').bind('click', _clickTestFn);
var panel = this.getPanel(), tree = this.find('.tree-holder');
// set the panel to the bottom of the field. Takes into account the
// mouse scroll position.
// @todo - support for opening above content
var scrollTop = 0;
this.parents().each(function(i, e) {
if($(e).scrollTop() > 0) {
scrollTop = $(e).scrollTop();
return;
}
});
var top = this.position().top + this.height() + scrollTop;
panel.css('top', top);
panel.css('width', this.width());
panel.show();
// swap the down arrow with an up arrow
var toggle = this.find(".treedropdownfield-toggle-panel-link");
toggle.addClass('treedropdownfield-open-tree');
this.addClass("treedropdownfield-open-tree");
toggle.find("a")
.removeClass('ui-icon-triangle-1-s')
.addClass('ui-icon-triangle-1-n');
if(tree.is(':empty')) this.loadTree();
},
closePanel: function() {
jQuery('body').unbind('click', _clickTestFn);
// swap the up arrow with a down arrow
var toggle = this.find(".treedropdownfield-toggle-panel-link");
toggle.removeClass('treedropdownfield-open-tree');
this.removeClass('treedropdownfield-open-tree');
toggle.find("a")
.removeClass('ui-icon-triangle-1-n')
.addClass('ui-icon-triangle-1-s');
this.getPanel().hide();
},
togglePanel: function() {
this[this.getPanel().is(':visible') ? 'closePanel' : 'openPanel']();
},
setTitle: function(title) {
if(!title) title = strings.fieldTitle;
this.find('.treedropdownfield-title').html(title);
this.data('title', encodeURIComponent(title)); // separate view from storage (important for search cancellation)
},
getTitle: function() {
return this.find('.treedropdownfield-title').text();
},
/**
* Update title from tree node value
*/
updateTitle: function() {
var self = this, tree = self.find('.tree-holder'), val = this.getValue();
var updateFn = function() {
var val = self.getValue();
if(val) {
var node = tree.find('*[data-id="' + val + '"]'),
title = node.children('a').find("span.jstree_pageicon")?node.children('a').find("span.item").html():null;
if(!title) title=(node) ? tree.jstree('get_text', node[0]) : null;
if(title) self.setTitle(title);
if(node) tree.jstree('select_node', node);
}
};
// Load the tree if its not already present
if(jQuery.jstree._reference(tree) || !val) updateFn();
else this.loadTree(null, updateFn);
},
setValue: function(val) {
this.find(':input:hidden').val(val).trigger('change');
},
getValue: function() {
return this.find(':input:hidden').val();
},
loadTree: function(params, callback) {
var self = this, panel = this.getPanel(), treeHolder = $(panel).find('.tree-holder');
var params = (params) ? $.extend({}, this.getRequestParams(), params) : this.getRequestParams();
panel.addClass('loading');
treeHolder.load(this.data('urlTree'), params, function(html, status, xhr) {
var firstLoad = true;
if(status == 'success') {
$(this)
.jstree('destroy')
.bind('loaded.jstree', function(e, data) {
var val = self.getValue(), selectNode = treeHolder.find('*[data-id="' + val + '"]'),
currentNode = data.inst.get_selected();
if(val && selectNode != currentNode) data.inst.select_node(selectNode);
firstLoad = false;
if(callback) callback.apply(self);
})
.jstree(self.getTreeConfig())
.bind('select_node.jstree', function(e, data) {
var node = data.rslt.obj, id = $(node).data('id');
if(!firstLoad && !self.getValue() == id) {
// Value is already selected, unselect it (for lack of a better UI to do this)
self.data('metadata', null);
self.setTitle(null);
self.setValue(null);
data.inst.deselect_node(node);
} else {
self.data('metadata', $.extend({id: id}, $(node).getMetaData()));
self.setTitle(data.inst.get_text(node));
self.setValue(id);
}
// Avoid auto-closing panel on first load
if(!firstLoad) self.closePanel();
firstLoad=false
});
}
panel.removeClass('loading');
});
},
getTreeConfig: function() {
var self = this;
return {
'core': {
// 'initially_open': ['record-0'],
'animation': 0
},
'html_data': {
// TODO Hack to avoid ajax load on init, see http://code.google.com/p/jstree/issues/detail?id=911
'data': this.getPanel().find('.tree-holder').html(),
'ajax': {
'url': this.data('urlTree'),
'data': function(node) {
var id = $(node).data("id") ? $(node).data("id") : 0, params = self.getRequestParams();
params = $.extend({}, params, {ID: id, ajax: 1});
return params;
}
}
},
'ui': {
"select_limit" : 1,
'initially_select': [this.getPanel().find('.current').attr('id')]
},
'themes': {
'theme': 'apple'
},
'plugins': ['html_data', 'ui', 'themes']
};
},
/**
* If the field is contained in a form, submit all form parameters by default.
* This is useful to keep state like locale values which are typically
* encoded in hidden fields through the form.
*
* @return {object}
*/
getRequestParams: function() {
return {};
}
});
$('.TreeDropdownField .tree-holder li').entwine({
/**
* Overload to return more data. The same data should be set on initial
* value through PHP as well (see TreeDropdownField->Field()).
*
* @return {object}
*/
getMetaData: function() {
var matches = this.attr('class').match(/class-([^\s]*)/i);
var klass = matches ? matches[1] : '';
return {ClassName: klass};
}
});
$('.TreeDropdownField *').entwine({
getField: function() {
return this.parents('.TreeDropdownField:first');
}
});
$('.TreeDropdownField').entwine({
onclick: function(e) {
this.togglePanel();
return false;
}
});
$('.TreeDropdownField .treedropdownfield-panel').entwine({
onclick: function(e) {
return false;
}
});
$('.TreeDropdownField.searchable').entwine({
onadd: function() {
this._super();
var title = this.data('title');
this.find('.treedropdownfield-title').replaceWith(
$('<input type="text" class="treedropdownfield-title search" />')
);
this.setTitle(title ? title : strings.searchFieldTitle);
},
setTitle: function(title) {
if(!title && title !== '') title = strings.fieldTitle;
this.find('.treedropdownfield-title').val(title);
},
getTitle: function() {
return this.find('.treedropdownfield-title').val();
},
search: function(str, callback) {
this.openPanel();
this.loadTree({search: str}, callback);
},
cancelSearch: function() {
this.closePanel();
this.loadTree();
this.setTitle(this.data('title'));
}
});
$('.TreeDropdownField.searchable input.search').entwine({
onfocusin: function(e) {
var field = this.getField();
field.setTitle('');
},
onfocusout: function(e) {
var field = this.getField();
if(!field.getTitle()) field.setTitle(false);
},
onkeydown: function(e) {
var field = this.getField();
if(e.keyCode == 13) {
// trigger search on ENTER key
field.search(this.val());
return false;
} else if(e.keyCode == 27) {
// cancel search on ESC key
field.cancelSearch();
}
}
});
$('.TreeDropdownField.multiple').entwine({
getTreeConfig: function() {
var cfg = this._super();
cfg.checkbox = {override_ui: true, two_state: true};
cfg.plugins.push('checkbox');
cfg.ui.select_limit = -1;
return cfg;
},
loadTree: function(params, callback) {
var self = this, panel = this.getPanel(), treeHolder = $(panel).find('.tree-holder');
var params = (params) ? $.extend({}, this.getRequestParams(), params) : this.getRequestParams();
panel.addClass('loading');
treeHolder.load(this.data('urlTree'), params, function(html, status, xhr) {
var firstLoad = true;
if(status == 'success') {
$(this)
.jstree('destroy')
.bind('loaded.jstree', function(e, data) {
$.each(self.getValue(), function(i, val) {
data.inst.check_node(treeHolder.find('*[data-id=' + val + ']'));
});
firstLoad = false;
if(callback) callback.apply(self);
})
.jstree(self.getTreeConfig())
.bind('uncheck_node.jstree check_node.jstree', function(e, data) {
var nodes = data.inst.get_checked(null, true);
self.setValue($.map(nodes, function(el, i) {
return $(el).data('id');
}));
self.setTitle($.map(nodes, function(el, i) {
return data.inst.get_text(el);
}));
self.data('metadata', $.map(nodes, function(el, i) {
return {id: $(el).data('id'), metadata: $(el).getMetaData()};
}));
});
}
panel.removeClass('loading');
});
},
getValue: function() {
var val = this._super();
return val.split(/ *, */);
},
setValue: function(val) {
this._super($.isArray(val) ? val.join(',') : val);
},
setTitle: function(title) {
this._super($.isArray(title) ? title.join(', ') : title);
},
updateTitle: function() {
// TODO Not supported due to multiple values/titles yet
}
});
$('.TreeDropdownField input[type=hidden]').entwine({
onadd: function() {
this.bind('change.TreeDropdownField', function() {
$(this).getField().updateTitle();
});
},
onremove: function() {
this.unbind('.TreeDropdownField');
}
});
});
}(jQuery));