(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({ // XMLHttpRequest CurrentXhr: null, onadd: function() { this.append( '' + '' + '
' ); 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'); 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') && !panel.hasClass('loading')) this.loadTree(); this.trigger('panelshow'); }, 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(); this.trigger('panelhide'); }, 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.length > 0) ? tree.jstree('get_text', node[0]) : null; if(title) { self.setTitle(title); self.data('title', title) } if(node) tree.jstree('select_node', node); } }; // Load the tree if its not already present if(!tree.is(':empty') || !val) updateFn(); else this.loadTree({forceValue: val}, updateFn); }, setValue: function(val) { this.data('metadata', $.extend(this.data('metadata'), {id: 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'), params = (params) ? $.extend({}, this.getRequestParams(), params) : this.getRequestParams(), xhr; if(this.getCurrentXhr()) this.getCurrentXhr().abort(); panel.addClass('loading'); xhr = $.ajax({ url: this.data('urlTree'), data: params, complete: function(xhr, status) { panel.removeClass('loading'); }, success: function(html, status, xhr) { treeHolder.html(html); var firstLoad = true; treeHolder .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; }); self.setCurrentXhr(null); } }); this.setCurrentXhr(xhr); }, 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': function(node) { var url = $.path.parseUrl(self.data('urlTree')).hrefNoSearch; return url + '/' + ($(node).data("id") ? $(node).data("id") : 0); }, 'data': function(node) { var query = $.query.load(self.data('urlTree')).keys; var params = self.getRequestParams(); params = $.extend({}, query, params, {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 = decodeURIComponent(this.data('title')); this.find('.treedropdownfield-title').replaceWith( $('') ); 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(); }, resetTitle: function() { this.setTitle(decodeURIComponent(this.data('title'))); }, search: function(str, callback) { this.openPanel(); this.loadTree({search: str}, callback); }, cancelSearch: function() { this.closePanel(); this.loadTree(); this.resetTitle(); } }); $('.TreeDropdownField.searchable input.search').entwine({ onfocusin: function(e) { var field = this.getField(); field.setTitle(''); }, onfocusout: function(e) { var field = this.getField(); field.resetTitle(); }, 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(), xhr; if(this.getCurrentXhr()) this.getCurrentXhr().abort(); panel.addClass('loading'); xhr = $.ajax({ url: this.data('urlTree'), data: params, complete: function(xhr, status) { panel.removeClass('loading'); }, success: function(html, status, xhr) { treeHolder.html(html); var firstLoad = true; self.setCurrentXhr(null); treeHolder .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()}; })); }); } }); this.setCurrentXhr(xhr); }, 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));