diff --git a/javascript/TreeDropdownField.js b/javascript/TreeDropdownField.js
new file mode 100644
index 000000000..da0042b1a
--- /dev/null
+++ b/javascript/TreeDropdownField.js
@@ -0,0 +1,237 @@
+(function($) {
+ $.entwine('ss', function($){
+
+ var strings = {
+ 'openlink': 'Open',
+ 'fieldTitle': '(choose)',
+ 'searchFieldTitle': '(choose or search)'
+ };
+
+ /**
+ * @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({
+ onmatch: function() {
+ this.append(
+ '' +
+ '' +
+ '
'
+ );
+ if(this.data('title')) this.setTitle(this.data('title'));
+ this.getPanel().hide();
+
+ this._super();
+ },
+ getPanel: function() {
+ return this.find('.panel');
+ },
+ openPanel: function() {
+ var panel = this.getPanel(), tree = this.find('.tree-holder');
+ panel.show();
+ if(tree.is(':empty')) this.loadTree();
+ },
+ closePanel: function() {
+ this.getPanel().hide();
+ },
+ togglePanel: function() {
+ this[this.getPanel().is(':visible') ? 'closePanel' : 'openPanel']();
+ },
+ setTitle: function(title) {
+ if(!title) title = strings.fieldTitle;
+
+ this.find('.title').text(title);
+ this.data('title', title); // separate view from storage (important for search cancellation)
+ },
+ getTitle: function() {
+ return this.find('.title').text();
+ },
+ setValue: function(val) {
+ this.find(':input:hidden').val(val);
+ },
+ 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) ? this.getRequestParams().concat(params) : this.getRequestParams();
+ panel.addClass('loading');
+ treeHolder.load(this.data('url-tree'), params, function(html, status, xhr) {
+ var firstLoad = true;
+ if(status == 'success') {
+ $(this)
+ .bind('loaded.jstree', function(e, data) {
+ var val = self.getValue();
+ if(val) data.inst.select_node(treeHolder.find('*[data-id=' + val + ']'));
+ 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(self.getValue() == id) {
+ self.setValue(null);
+ self.setTitle(null);
+ } else {
+ self.setValue(id);
+ self.setTitle(data.inst.get_text(node));
+ }
+
+ // Avoid auto-closing panel on first load
+ if(!firstLoad) self.closePanel();
+ });
+ }
+
+ 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('url-tree'),
+ 'data': function(node) {
+ var id = $(node).data("id") ? $(node).data("id") : 0, params = self.getRequestParams();
+ params = params.concat([{name: 'ID', value: id}, {name: 'ajax', value: 1}]);
+ return params;
+ }
+ }
+ },
+ 'ui': {
+ "select_limit" : 1,
+ 'initially_select': [this.getPanel().find('.current').attr('id')]
+ },
+ 'plugins': ['html_data', 'ui', 'themes']
+ // 'plugins': ['html_data', 'ui', 'themeroller']
+ };
+ },
+ /**
+ * 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 {array}
+ */
+ getRequestParams: function() {
+ var form = this.parents('form');
+ return form.length ? form.serializeArray() : [];
+ }
+ });
+ $('.TreeDropdownField *').entwine({
+ getField: function() {
+ return this.parents('.TreeDropdownField:first');
+ }
+ });
+ $('.TreeDropdownField .toggle-panel-link, .TreeDropdownField span.title').entwine({
+ onclick: function(e) {
+ this.getField().togglePanel();
+ return false;
+ }
+ });
+
+ $('.TreeDropdownField.searchable').entwine({
+ onmatch: function() {
+ this._super();
+
+ var title = this.data('title');
+ this.find('.title').replaceWith(
+ $('')
+ );
+ this.setTitle(title ? title : strings.searchFieldTitle);
+ },
+ setTitle: function(title) {
+ if(!title) title = strings.fieldTitle;
+
+ this.find('.title').val(title);
+ },
+ getTitle: function() {
+ return this.find('.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({
+ 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};
+ 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) ? this.getRequestParams().concat(params) : this.getRequestParams();
+ panel.addClass('loading');
+ treeHolder.load(this.data('url-tree'), params, function(html, status, xhr) {
+ var firstLoad = true;
+ if(status == 'success') {
+ $(this)
+ .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);
+ }));
+ });
+ }
+
+ 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);
+ }
+ });
+ });
+}(jQuery));
\ No newline at end of file
diff --git a/security/Group.php b/security/Group.php
index df9e27dab..83db2c043 100644
--- a/security/Group.php
+++ b/security/Group.php
@@ -35,6 +35,12 @@ class Group extends DataObject {
"Hierarchy",
);
+ function populateDefaults() {
+ parent::populateDefaults();
+
+ if(!$this->Title) $this->Title = _t('SecurityAdmin.NEWGROUP',"New Group");
+ }
+
function getAllChildren() {
$doSet = new DataObjectSet();