Skip to content

Instantly share code, notes, and snippets.

@scho
Created August 1, 2012 11:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save scho/3226088 to your computer and use it in GitHub Desktop.
Save scho/3226088 to your computer and use it in GitHub Desktop.
Netzke component implementation of Ext.tree.Panel with rails 3.2.7 using netzke-core/basepack 0.7.6 and Ext JS 4.0.2
// Extends Ext.data.TreeStore and adds paging to it
Ext.define('Ext.netzke.PagingTreeStore', {
extend: 'Ext.data.TreeStore',
alias: 'pagingtreestore',
currentPage: 1,
config:{
totalCount: null,
pageSize: null
},
// Load a specific Page
loadPage: function(page){
var me = this;
me.currentPage = page;
me.read({
page: page,
start: (page - 1) * me.pageSize,
limit: me.pageSize
});
},
// Load next Page
nextPage: function(){
this.loadPage(this.currentPage + 1);
},
// Load previous Page
previousPage: function(){
this.loadPage(this.currentPage - 1);
},
// Overwrite function in order to set totalCount
onProxyLoad: function(operation) {
// This method must be overwritten in order to set totalCount
var me = this,
resultSet = operation.getResultSet(),
node = operation.node;
// If the node doesn't have a parent node, set totalCount
if (resultSet && node.parentNode == null) {
me.setTotalCount(resultSet.total);
}
// We're done here, call parent
this.callParent(arguments);
},
getCount : function(){
return this.getRootNode().childNodes.length;
},
getRange : function(start, end){
var me = this,
items = this.getRootNode().childNodes,
range = [],
i;
if (items.length < 1) {
return range;
}
start = start || 0;
end = Math.min(typeof end == 'undefined' ? items.length - 1 : end, items.length - 1);
if (start <= end) {
for (i = start; i <= end; i++) {
range[range.length] = items[i];
}
} else {
for (i = start; i >= end; i--) {
range[range.length] = items[i];
}
}
return range;
}
});
new Object({
trackMouseOver: true,
loadMask: true,
componentLoadMask: {msg: "Loading..."},
deleteMaskMsg: "Deleting...",
initComponent: function(){
var metaColumn;
var fields = this.extraFields; // field configs for the underlying data model
this.plugins = this.plugins || [];
this.features = this.features || [];
// Enable filters feature
//this.features.push({
// encode: true,
// ftype: 'filters'
//});
// Run through columns and set up different configuration for each
Ext.each(this.columns, function(c, i){
this.normalizeRenderer(c);
// Build the field configuration for this column
var fieldConfig = {name: c.name, defaultValue: c.defaultValue};
if (c.name !== '_meta') fieldConfig.type = this.fieldTypeForAttrType(c.attrType); // field type (grid editors need this to function well)
if (c.attrType == 'datetime') {
fieldConfig.dateFormat = 'Y-m-d g:i:s'; // in this format we receive dates from the server
if (!c.renderer) {
c.renderer = Ext.util.Format.dateRenderer(c.format || fieldConfig.dateFormat); // format in which the data will be rendered
}
};
if (c.attrType == 'date') {
fieldConfig.dateFormat = 'Y-m-d'; // in this format we receive dates from the server
if (!c.renderer) {
c.renderer = Ext.util.Format.dateRenderer(c.format || fieldConfig.dateFormat); // format in which the data will be rendered
}
};
fields.push(fieldConfig);
// We will not use meta columns as actual columns (not even hidden) - only to create the records
if (c.meta) {
metaColumn = c;
return;
}
// Set rendeder for association columns (the one displaying associations by the specified method instead of id)
if (c.assoc) {
// Editor for association column
c.editor = Ext.apply({
parentId: this.id,
name: c.name,
selectOnFocus: true // ?
}, c.editor);
// Renderer for association column
this.normalizeAssociationRenderer(c);
}
if (c.editor) {
Ext.applyIf(c.editor, {selectOnFocus: true});
}
// Setting the default filter type
if (c.filterable && !c.filter) {
c.filter = {type: this.fieldTypeForAttrType(c.attrType)};
}
// setting dataIndex
c.dataIndex = c.name;
// HACK: somehow this is not set by Ext (while it should be)
if (c.xtype == 'datecolumn') c.format = c.format || Ext.util.Format.dateFormat;
}, this);
/* ... and done with the columns */
// Define the model
Ext.define(this.id, {
extend: 'Ext.data.Model',
idProperty: this.pri, // Primary key
fields: fields
});
// After we created the record (model), we can get rid of the meta column
Ext.Array.remove(this.columns, metaColumn);
// Prepare column model config with columns in the correct order; columns out of order go to the end.
var colModelConfig = [];
var columns = this.columns;
Ext.each(this.columns, function(c) {
var mainColConfig;
Ext.each(this.columns, function(oc) {
if (c.name === oc.name) {
mainColConfig = Ext.apply({}, oc);
return false;
}
});
colModelConfig.push(Ext.apply(mainColConfig, c));
}, this);
// We don't need original columns any longer
delete this.columns;
// ... instead, define own column model
this.columns = colModelConfig;
// DirectProxy that uses our Ext.direct provider
var proxy = {
type: 'direct',
directFn: Netzke.providers[this.id].getData,
reader: {
type: 'json',
root: 'data'
},
listeners: {
exception: {
fn: this.loadExceptionHandler,
scope: this
},
load: { // Netzke-introduced event; this will also be fired when an exception occurs.
fn: function(proxy, response, operation) {
// besides getting data into the store, we may also get commands to execute
response = response.result;
if (response) { // or did we have an exception?
Ext.each(['data', 'total', 'success'], function(property){delete response[property];});
this.bulkExecute(response);
}
},
scope: this
}
}
}
// Create the netzke PagingTreeStore
this.store = Ext.create('Ext.netzke.PagingTreeStore', {
model: this.id,
proxy: proxy,
root: this.inlineData || [],
pageSize: this.rowsPerPage
});
// HACK: we must let the store now totalCount
this.store.setTotalCount(this.inlineData && this.inlineData[this.store.getProxy().getReader().totalProperty]);
// Drag'n'Drop
if (this.enableRowsReordering){
this.ddPlugin = new Ext.ux.dd.GridDragDropRowOrder({
scrollable: true // enable scrolling support (default is false)
});
this.plugins.push(this.ddPlugin);
}
// Cell editing
if (!this.prohibitUpdate) {
this.plugins.push(Ext.create('Ext.grid.plugin.CellEditing', {pluginId: 'celleditor'}));
}
// Toolbar
this.dockedItems = this.dockedItems || [];
if (this.enablePagination) {
this.dockedItems.push({
xtype: 'pagingtoolbar',
dock: 'bottom',
store: this.store,
items: this.bbar && ["-"].concat(this.bbar) // append the old bbar. TODO: get rid of it.
});
} else if (this.bbar) {
this.dockedItems.push({
xtype: 'toolbar',
dock: 'bottom',
items: this.bbar
});
}
delete this.bbar;
// Now let Ext.grid.EditorGridPanel do the rest (original initComponent)
this.callParent();
// Context menu
if (this.contextMenu) {
this.on('itemcontextmenu', this.onItemContextMenu, this);
}
// Disabling/enabling editInForm button according to current selection
if (this.enableEditInForm && !this.prohibitUpdate) {
this.getSelectionModel().on('selectionchange', function(selModel, selected){
var disabled;
if (selected.length === 0) { // empty?
disabled = true;
} else {
// Disable "edit in form" button if new record is present in selection
Ext.each(selected, function(r){
if (r.isNew) { disabled = true; return false; }
});
};
this.actions.editInForm.setDisabled(disabled);
}, this);
}
// Process selectionchange event to enable/disable actions
this.getSelectionModel().on('selectionchange', function(selModel){
if (this.actions.del) this.actions.del.setDisabled(!selModel.hasSelection() || this.prohibitDelete);
if (this.actions.edit) this.actions.edit.setDisabled(selModel.getCount() != 1 || this.prohibitUpdate);
}, this);
// Drag n Drop event
if (this.enableRowsReordering){
this.ddPlugin.on('afterrowmove', this.onAfterRowMove, this);
}
// WIP: GridView
this.getView().getRowClass = this.defaultGetRowClass;
// When starting editing as assocition column, pre-load the combobox store from the meta column, so that we don't see the real value of this cell (the id of the associated record), but rather the associated record by the configured method.
this.on('beforeedit', function(e){
if (e.column.assoc && e.record.get('_meta')) {
var data = [e.record.get(e.field), e.record.get('_meta').associationValues[e.field]],
store = e.column.getEditor().store;
if (store.getCount() === 0) {
store.loadData([data]);
}
}
}, this);
this.on('afterrender', function() {
// Persistence-related events (afterrender to avoid blank event firing on render)
if (this.persistence) {
// Inform the server part about column operations
this.on('columnresize', this.onColumnResize, this);
this.on('columnmove', this.onColumnMove, this);
this.on('columnhide', this.onColumnHide, this);
this.on('columnshow', this.onColumnShow, this);
}
}, this);
},
fieldTypeForAttrType: function(attrType){
var map = {
integer : 'int',
decimal : 'float',
datetime : 'date',
date : 'date',
string : 'string',
text : 'string',
'boolean' : 'boolean'
};
return map[attrType] || 'string';
},
// Normalizes the renderer for a column.
// Renderer may be:
// 1) a string that contains the name of the function to be used as renderer.
// 2) an array, where the first element is the function name, and the rest - the arguments
// that will be passed to that function along with the value to be rendered.
// The function is searched in the following objects: 1) Ext.util.Format, 2) this.
// If not found, it is simply evaluated. Handy, when as renderer we receive an inline JS function,
// or reference to a function in some other scope.
// So, these will work:
// * "uppercase"
// * ["ellipsis", 10]
// * ["substr", 3, 5]
// * "myRenderer" (if this.myRenderer is a function)
// * ["Some.scope.Format.customRenderer", 10, 20, 30] (if Some.scope.Format.customRenderer is a function)
// * "function(v){ return 'Value: ' + v; }"
normalizeRenderer: function(c) {
if (!c.renderer) return;
var name, args = [];
if ('string' === typeof c.renderer) {
name = c.renderer.camelize(true);
} else {
name = c.renderer[0];
args = c.renderer.slice(1);
}
// First check whether Ext.util.Format has it
if (Ext.isFunction(Ext.util.Format[name])) {
c.renderer = Ext.Function.bind(Ext.util.Format[name], this, args, 1);
} else if (Ext.isFunction(this[name])) {
// ... then if our own class has it
c.renderer = Ext.Function.bind(this[name], this, args, 1);
} else {
// ... and, as last resort, evaluate it (allows passing inline javascript function as renderer)
eval("c.renderer = " + c.renderer + ";");
}
},
/*
Set a renderer that displayes association values instead of association record ID.
The association values are passed in the meta-column under associationValues hash.
*/
normalizeAssociationRenderer: function(c) {
c.scope = this;
var passedRenderer = c.renderer; // renderer we got from normalizeRenderer
c.renderer = function(value, a, r, ri, ci){
var column = this.headerCt.items.getAt(ci),
editor = column.getEditor && column.getEditor(),
// HACK: using private property 'store'
recordFromStore = editor && editor.isXType('combobox') && editor.store.findRecord('field1', value),
renderedValue;
if (recordFromStore) {
renderedValue = recordFromStore.get('field2');
} else if (c.assoc && r.get('_meta')) {
renderedValue = r.get('_meta').associationValues[c.name] || value;
} else {
renderedValue = value;
}
return passedRenderer ? passedRenderer.call(this, renderedValue) : renderedValue;
};
}
})
# Netzke Module
module Netzke
# The Netzke Communitypack
module Communitypack
# Netzke class for the Ext JS TreePanel
class TreePanel < Netzke::Base
# Ext JS base class
js_base_class "Ext.tree.TreePanel"
js_include :paging_tree_store
js_mixin :tree_panel
# Include data accessor module
include Netzke::Basepack::DataAccessor
# Include columns module
include Netzke::Basepack::GridPanel::Columns
extend ActiveSupport::Memoizable
class_config_option :default_instance_config, {
:indicate_leafs => true,
:auto_scroll => false,
:root_visible => false,
:load_inline_data => true,
:enable_pagination => true,
:rows_per_page => 30,
:extra_fields => [{:name => 'leaf', :type => 'boolean'},{:name => 'expanded', :type => 'boolean'}]
}
# Override the js config
#
# @return [Hash]
def js_config
res = super
res.merge({
:title => res[:title] || self.class.js_properties[:title] || data_class.name.pluralize,
:bbar => config.has_key?(:bbar) ? config[:bbar] : {},
:context_menu => config.has_key?(:context_menu) ? config[:context_menu] : {},
:columns => columns(:with_meta => true), # columns
:columns_order => columns_order,
:model => config[:model], # the model name
:inline_data => (get_data if config[:load_inline_data]), # inline data (loaded along with the grid panel)
:pri => data_class.primary_key
})
end
# @!method get_data_endpoint
#
# Returns something like:
# [
# { 'id'=> 1, 'text'=> 'A folder Node', 'leaf'=> false },
# { 'id'=> 2, 'text'=> 'A leaf Node', 'leaf'=> true }
# ]
#
# @param [Hash] params
endpoint :get_data do |params|
get_data(params)
end
# Method that is called by the get_data endpoint
# Calls the get_children method and returns the serialized records
#
# @param [] *args takes any arguments
# @return [Hash] all the serialized data
def get_data(*args)
params = args.first || {} # params are optional!
{}.tap do |res|
# get children
records = get_children(params)
# Serialize children
res[:data] = serialize_data(records)
begin
# Set total_entries
res[:total] = records.total_entries if config[:enable_pagination]
rescue
# if it's not the root collection, records.total_entries will fail and we don't need it!
end
end
end
# Serializes an array of objects
#
# @param [Array] records
# @return [Array] the serialized data
def serialize_data(records)
records.map { |r|
r.to_hash(columns(:with_meta => true)).tap { |h|
config[:extra_fields].each do |f|
name = f[:name].underscore.to_sym
h[name] = send("#{name}#{f[:type] == 'boolean' ? '?' : ''}", r)
end
inline_children = get_inline_children(r)
h[:data] = serialize_data(inline_children) unless inline_children.nil?
}
}
end
# Retrieves all children for a node
# NOTE: It's recommended to overwrite this method
#
# @param [Hash] params
# @return [Array] array of records
def get_children(params)
if params[:node] == 'root'
data_class.find_by_parent_id(nil)
else
data_class.find_by_parent_id(params[:node])
end
end
# Should return all children of the record that should also be serialized in the current request
# NOTE: It's recommended to overwrite this method
#
# @param [Object] r The record for which the inline children should be loaded
# @return [NilClass, Array] If nil is returned, the tree doesn't know anything about any children, so opening the node will cause another request.
# If an empty array is returned, the tree assumes that there are no children available for this node (and thus you can't open it!)
def get_inline_children(r)
nil
end
# Is the record a leaf or not?
# NOTE: It's recommended to overwrite this method
#
# @param [Object] r
# @return [Boolean] Whether the node is a leaf or not
def leaf?(r)
r.children.empty?
end
# Is the record a expanded or not?
# NOTE: It's recommended to overwrite this method
#
# @param [Object] r
# @return [Boolean] Whether the node is expanded or not
def expanded?(r)
false
end
# Get all the association values for a specific record
#
# @param [Object] record
# @return [Array]
def get_association_values(record)
columns.select{ |c| c[:name].index("__") }.each.inject({}) do |r,c|
r.merge(c[:name] => record.value_for_attribute(c, true))
end
end
# The default association values
#
# @return [Array]
def get_default_association_values
columns.select{ |c| c[:name].index("__") && c[:default_value] }.each.inject({}) do |r,c|
assoc, assoc_method = assoc_and_assoc_method_for_attr(c)
assoc_instance = assoc.klass.find(c[:default_value])
r.merge(c[:name] => assoc_instance.send(assoc_method))
end
end
memoize :get_default_association_values
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment