Created
August 1, 2012 11:43
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
} | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
}; | |
} | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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