frappe_docker/frappe-bench/sites/assets/js/report.min.js
2017-07-31 15:51:51 +05:30

8590 lines
256 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

frappe.views.ReportFactory = frappe.views.Factory.extend({
make: function make(route) {
new frappe.views.ReportViewPage(route[1], route[2]);
}
});
frappe.views.ReportViewPage = Class.extend({
init: function init(doctype, docname) {
if (!frappe.model.can_get_report(doctype)) {
frappe.show_not_permitted(frappe.get_route_str());
return;
}
this.doctype = doctype;
this.docname = docname;
this.page_name = frappe.get_route_str();
this.make_page();
var me = this;
frappe.model.with_doctype(this.doctype, function () {
me.make_report_view();
if (me.docname) {
frappe.model.with_doc('Report', me.docname, function (r) {
me.parent.reportview.set_columns_and_filters(JSON.parse(frappe.get_doc("Report", me.docname).json || '{}'));
me.parent.reportview.set_route_filters();
me.parent.reportview.run();
});
} else {
me.parent.reportview.set_route_filters();
me.parent.reportview.run();
}
});
},
make_page: function make_page() {
var me = this;
this.parent = frappe.container.add_page(this.page_name);
frappe.ui.make_app_page({ parent: this.parent, single_column: true });
this.page = this.parent.page;
frappe.container.change_to(this.page_name);
$(this.parent).on('show', function () {
if (me.parent.reportview.set_route_filters()) {
me.parent.reportview.run();
}
});
},
make_report_view: function make_report_view() {
this.page.set_title(__(this.doctype));
var module = locals.DocType[this.doctype].module;
frappe.breadcrumbs.add(module, this.doctype);
this.parent.reportview = new frappe.views.ReportView({
doctype: this.doctype,
docname: this.docname,
parent: this.parent
});
}
});
frappe.views.ReportView = frappe.ui.BaseList.extend({
init: function init(opts) {
var me = this;
$.extend(this, opts);
this.can_delete = frappe.model.can_delete(me.doctype);
this.tab_name = '`tab' + this.doctype + '`';
this.setup();
},
setup: function setup() {
var me = this;
this.add_totals_row = 0;
this.page = this.parent.page;
this.meta = frappe.get_meta(this.doctype);
this._body = $('<div>').appendTo(this.page.main);
this.page_title = __('Report') + ': ' + (this.docname ? __(this.doctype) + ' - ' + __(this.docname) : __(this.doctype));
this.page.set_title(this.page_title);
this.init_user_settings();
this.make({
page: this.parent.page,
method: 'frappe.desk.reportview.get',
save_user_settings: true,
get_args: this.get_args,
parent: this._body,
start: 0,
show_filters: true,
allow_delete: true
});
this.make_new_and_refresh();
this.make_delete();
this.make_column_picker();
this.make_sorter();
this.make_totals_row_button();
this.setup_print();
this.make_export();
this.setup_auto_email();
this.set_init_columns();
this.make_save();
this.make_user_permissions();
this.set_tag_and_status_filter();
this.setup_listview_settings();
this.page.add_menu_item(__("Add to Desktop"), function () {
frappe.add_to_desktop(me.docname || __('{0} Report', [me.doctype]), me.doctype, me.docname);
}, true);
},
make_new_and_refresh: function make_new_and_refresh() {
var me = this;
this.page.set_primary_action(__("Refresh"), function () {
me.run();
});
this.page.add_menu_item(__("New {0}", [this.doctype]), function () {
me.make_new_doc(me.doctype);
}, true);
},
setup_auto_email: function setup_auto_email() {
var me = this;
this.page.add_menu_item(__("Setup Auto Email"), function () {
if (me.docname) {
frappe.set_route('List', 'Auto Email Report', { 'report': me.docname });
} else {
frappe.msgprint({ message: __('Please save the report first'), indicator: 'red' });
}
}, true);
},
set_init_columns: function set_init_columns() {
var me = this;
var columns = [];
if (this.user_settings.fields && !this.docname) {
this.user_settings.fields.forEach(function (field) {
var coldef = me.get_column_info_from_field(field);
if (!in_list(['_seen', '_comments', '_user_tags', '_assign', '_liked_by', 'docstatus'], coldef[0])) {
columns.push(coldef);
}
});
}
if (!columns.length) {
var columns = [['name', this.doctype]];
$.each(frappe.meta.docfield_list[this.doctype], function (i, df) {
if ((df.in_standard_filter || df.in_list_view) && df.fieldname != 'naming_series' && !in_list(frappe.model.no_value_type, df.fieldtype) && !df.report_hide) {
columns.push([df.fieldname, df.parent]);
}
});
}
this.set_columns(columns);
this.page.footer.on('click', '.show-all-data', function () {
me.show_all_data = $(this).prop('checked');
me.run();
});
},
set_columns: function set_columns(columns) {
this.columns = columns;
this.column_info = this.get_columns();
this.refresh_footer();
},
refresh_footer: function refresh_footer() {
var can_write = frappe.model.can_write(this.doctype);
var has_child_column = this.has_child_column();
this.page.footer.empty();
if (can_write || has_child_column) {
$(frappe.render_template('reportview_footer', {
has_child_column: has_child_column,
can_write: can_write,
show_all_data: this.show_all_data
})).appendTo(this.page.footer);
this.page.footer.removeClass('hide');
} else {
this.page.footer.addClass('hide');
}
},
set_columns_and_filters: function set_columns_and_filters(opts) {
var me = this;
this.filter_list.clear_filters();
if (opts.columns) {
this.set_columns(opts.columns);
}
if (opts.filters) {
$.each(opts.filters, function (i, f) {
var df = frappe.meta.get_docfield(f[0], f[1]);
if (df && df.fieldtype == "Check") {
var value = f[3] ? "Yes" : "No";
} else {
var value = f[3];
}
me.filter_list.add_filter(f[0], f[1], f[2], value);
});
}
if (opts.add_total_row) {
this.add_total_row = opts.add_total_row;
}
if (opts.sort_by) this.sort_by_select.val(opts.sort_by);
if (opts.sort_order) this.sort_order_select.val(opts.sort_order);
if (opts.sort_by_next) this.sort_by_next_select.val(opts.sort_by_next);
if (opts.sort_order_next) this.sort_order_next_select.val(opts.sort_order_next);
this.add_totals_row = cint(opts.add_totals_row);
},
set_route_filters: function set_route_filters() {
var me = this;
if (frappe.route_options) {
this.set_filters_from_route_options({ clear_filters: this.docname ? false : true });
return true;
} else if (this.user_settings && this.user_settings.filters && !this.docname && this.user_settings.updated_on != this.user_settings_updated_on) {
this.filter_list.clear_filters();
$.each(this.user_settings.filters, function (i, f) {
me.filter_list.add_filter(f[0], f[1], f[2], f[3]);
});
return true;
}
this.user_settings_updated_on = this.user_settings.updated_on;
},
setup_print: function setup_print() {
var me = this;
this.page.add_menu_item(__("Print"), function () {
frappe.ui.get_print_settings(false, function (print_settings) {
var title = __(me.docname || me.doctype);
frappe.render_grid({ grid: me.grid, title: title, print_settings: print_settings });
});
}, true);
},
get_args: function get_args() {
var me = this;
var filters = this.filter_list ? this.filter_list.get_filters() : [];
return {
doctype: this.doctype,
fields: $.map(this.columns || [], function (v) {
return me.get_full_column_name(v);
}),
order_by: this.get_order_by(),
add_total_row: this.add_total_row,
filters: filters,
save_user_settings_fields: 1,
with_childnames: 1,
file_format_type: this.file_format_type
};
},
get_order_by: function get_order_by() {
var order_by = [];
var sort_by_select = this.get_selected_table_and_column(this.sort_by_select);
if (sort_by_select) {
order_by.push(sort_by_select + " " + this.sort_order_select.val());
}
if (this.sort_by_next_select && this.sort_by_next_select.val()) {
order_by.push(this.get_selected_table_and_column(this.sort_by_next_select) + ' ' + this.sort_order_next_select.val());
}
return order_by.join(", ");
},
get_selected_table_and_column: function get_selected_table_and_column(select) {
if (!select) {
return;
}
return select.selected_doctype ? this.get_full_column_name([select.selected_fieldname, select.selected_doctype]) : "";
},
get_full_column_name: function get_full_column_name(v) {
if (!v) return;
return (v[1] ? '`tab' + v[1] + '`' : this.tab_name) + '.`' + v[0] + '`';
},
get_column_info_from_field: function get_column_info_from_field(t) {
if (t.indexOf('.') === -1) {
return [strip(t, '`'), this.doctype];
} else {
var parts = t.split('.');
return [strip(parts[1], '`'), strip(parts[0], '`').substr(3)];
}
},
build_columns: function build_columns() {
var me = this;
return $.map(this.columns, function (c) {
var docfield = frappe.meta.docfield_map[c[1] || me.doctype][c[0]];
if (!docfield) {
var docfield = frappe.model.get_std_field(c[0]);
if (docfield) {
docfield.parent = me.doctype;
if (c[0] == "name") {
docfield.options = me.doctype;
}
}
}
if (!docfield) return;
var coldef = {
id: c[0],
field: c[0],
docfield: docfield,
name: __(docfield ? docfield.label : toTitle(c[0])),
width: (docfield ? cint(docfield.width) : 120) || 120,
formatter: function formatter(row, cell, value, columnDef, dataContext, for_print) {
var docfield = columnDef.docfield;
docfield.fieldtype = {
"_user_tags": "Tag",
"_comments": "Comment",
"_assign": "Assign",
"_liked_by": "LikedBy"
}[docfield.fieldname] || docfield.fieldtype;
if (docfield.fieldtype === "Link" && docfield.fieldname !== "name") {
if (!columnDef.report_docfield) {
columnDef.report_docfield = copy_dict(docfield);
}
docfield = columnDef.report_docfield;
docfield.link_onclick = repl('frappe.container.page.reportview.filter_or_open("%(parent)s", "%(fieldname)s", "%(value)s")', { parent: docfield.parent, fieldname: docfield.fieldname, value: value });
}
return frappe.format(value, docfield, { for_print: for_print, always_show_decimals: true }, dataContext);
}
};
return coldef;
});
},
filter_or_open: function filter_or_open(parent, fieldname, value) {
var filter_set = false;
this.filter_list.get_filters().forEach(function (f) {
if (f[1] === fieldname) {
filter_set = true;
}
});
if (!filter_set) {
this.set_filter(fieldname, value, false, false, parent);
} else {
var df = frappe.meta.get_docfield(parent, fieldname);
if (df.fieldtype === 'Link') {
frappe.set_route('Form', df.options, value);
}
}
},
render_view: function render_view() {
var me = this;
var data = this.get_unique_data(this.column_info);
this.set_totals_row();
$.each(data, function (i, v) {
v._idx = i + 1;
v.id = v._idx;
});
var options = {
enableCellNavigation: true,
enableColumnReorder: false
};
if (this.slickgrid_options) {
$.extend(options, this.slickgrid_options);
}
this.dataView = new Slick.Data.DataView();
this.set_data(data);
var grid_wrapper = this.wrapper.find('.result-list').addClass("slick-wrapper");
if (!options.autoHeight) grid_wrapper.css('height', '500px');
this.grid = new Slick.Grid(grid_wrapper.get(0), this.dataView, this.column_info, options);
if (!frappe.dom.is_touchscreen()) {
this.grid.setSelectionModel(new Slick.CellSelectionModel());
this.grid.registerPlugin(new Slick.CellExternalCopyManager({
dataItemColumnValueExtractor: function dataItemColumnValueExtractor(item, columnDef, value) {
return item[columnDef.field];
}
}));
}
frappe.slickgrid_tools.add_property_setter_on_resize(this.grid);
if (this.start != 0 && !options.autoHeight) {
this.grid.scrollRowIntoView(data.length - 1);
}
this.grid.onDblClick.subscribe(function (e, args) {
var row = me.dataView.getItem(args.row);
var cell = me.grid.getColumns()[args.cell];
me.edit_cell(row, cell.docfield);
});
this.dataView.onRowsChanged.subscribe(function (e, args) {
me.grid.invalidateRows(args.rows);
me.grid.render();
});
this.grid.onHeaderClick.subscribe(function (e, args) {
if (e.target.className === "slick-resizable-handle") return;
var df = args.column.docfield,
sort_by = df.parent + "." + df.fieldname;
if (sort_by === me.sort_by_select.val()) {
me.sort_order_select.val(me.sort_order_select.val() === "asc" ? "desc" : "asc");
} else {
me.sort_by_select.val(df.parent + "." + df.fieldname);
me.sort_order_select.val("asc");
}
me.run();
});
},
has_child_column: function has_child_column() {
var me = this;
return this.column_info.some(function (c) {
return c.docfield && c.docfield.parent !== me.doctype;
});
},
get_unique_data: function get_unique_data(columns) {
var me = this;
if (this.show_all_data || !this.has_child_column()) {
return this.data;
}
var data = [],
prev_row = null;
this.data.forEach(function (d) {
if (prev_row && d.name == prev_row.name) {
var new_row = {};
columns.forEach(function (c) {
if (!c.docfield || c.docfield.parent !== me.doctype) {
var val = d[c.field];
if (c.docfield && c.docfield.parent !== me.doctype) {
new_row[c.docfield.parent + ":name"] = d[c.docfield.parent + ":name"];
}
} else {
var val = '';
}
new_row[c.field] = val;
});
data.push(new_row);
} else {
data.push(d);
}
prev_row = d;
});
return data;
},
edit_cell: function edit_cell(row, docfield) {
if (!docfield || docfield.fieldname !== "idx" && frappe.model.std_fields_list.indexOf(docfield.fieldname) !== -1) {
return;
} else if (frappe.boot.user.can_write.indexOf(this.doctype) === -1) {
frappe.throw({ message: __("No permission to edit"), title: __('Not Permitted') });
}
var me = this;
var d = new frappe.ui.Dialog({
title: __("Edit") + " " + __(docfield.label),
fields: [docfield],
primary_action_label: __("Update"),
primary_action: function primary_action() {
me.update_value(docfield, d, row);
}
});
d.set_input(docfield.fieldname, row[docfield.fieldname]);
if (d.fields_list[0].disp_status != "Write") d.hide();else d.show();
},
update_value: function update_value(docfield, dialog, row) {
var me = this;
var args = {
doctype: docfield.parent,
name: row[docfield.parent === me.doctype ? "name" : docfield.parent + ":name"],
fieldname: docfield.fieldname,
value: dialog.get_value(docfield.fieldname)
};
if (!args.name) {
frappe.throw(__("ID field is required to edit values using Report. Please select the ID field using the Column Picker"));
}
frappe.call({
method: "frappe.client.set_value",
args: args,
callback: function callback(r) {
if (!r.exc) {
dialog.hide();
var doc = r.message;
$.each(me.dataView.getItems(), function (i, item) {
if (item.name === doc.name) {
var new_item = $.extend({}, item);
$.each(frappe.model.get_all_docs(doc), function (i, d) {
var name = item[d.doctype + ":name"];
if (!name) name = item.name;
if (name === d.name) {
for (var k in d) {
var v = d[k];
if (frappe.model.std_fields_list.indexOf(k) === -1 && item[k] !== undefined) {
new_item[k] = v;
}
}
}
});
me.dataView.updateItem(item.id, new_item);
}
});
}
}
});
},
set_data: function set_data(data) {
this.dataView.beginUpdate();
this.dataView.setItems(data);
this.dataView.endUpdate();
},
get_columns: function get_columns() {
var std_columns = [{ id: '_idx', field: '_idx', name: 'Sr.', width: 40, maxWidth: 40 }];
if (this.can_delete) {
std_columns = std_columns.concat([{
id: '_check', field: '_check', name: "", width: 30, maxWidth: 30,
formatter: function formatter(row, cell, value, columnDef, dataContext) {
return repl("<input type='checkbox' \
data-row='%(row)s' %(checked)s>", {
row: row,
checked: dataContext.selected ? "checked=\"checked\"" : ""
});
}
}]);
}
return std_columns.concat(this.build_columns());
},
make_column_picker: function make_column_picker() {
var me = this;
this.column_picker = new frappe.ui.ColumnPicker(this);
this.page.add_inner_button(__('Pick Columns'), function () {
me.column_picker.show(me.columns);
});
},
make_totals_row_button: function make_totals_row_button() {
var me = this;
this.page.add_inner_button(__('Show Totals'), function () {
me.add_totals_row = 1 - (me.add_totals_row ? me.add_totals_row : 0);
me.render_view();
});
},
set_totals_row: function set_totals_row() {
if (this.data.length && this.data[this.data.length - 1]._totals_row) {
this.data.pop();
}
if (this.add_totals_row) {
var totals_row = { _totals_row: 1 };
if (this.data.length) {
this.data.forEach(function (row, ri) {
$.each(row, function (key, value) {
if ($.isNumeric(value)) {
totals_row[key] = (totals_row[key] || 0) + value;
}
});
});
}
this.data.push(totals_row);
}
},
set_tag_and_status_filter: function set_tag_and_status_filter() {
var me = this;
this.wrapper.find('.result-list').on("click", ".label-info", function () {
if ($(this).attr("data-label")) {
me.set_filter("_user_tags", $(this).attr("data-label"));
}
});
this.wrapper.find('.result-list').on("click", "[data-workflow-state]", function () {
if ($(this).attr("data-workflow-state")) {
me.set_filter(me.state_fieldname, $(this).attr("data-workflow-state"));
}
});
},
make_sorter: function make_sorter() {
var me = this;
this.sort_dialog = new frappe.ui.Dialog({ title: __('Sorting Preferences') });
$(this.sort_dialog.body).html('<p class="help">' + __('Sort By') + '</p>\
<div class="sort-column"></div>\
<div><select class="sort-order form-control" style="margin-top: 10px; width: 60%;">\
<option value="asc">' + __('Ascending') + '</option>\
<option value="desc">' + __('Descending') + '</option>\
</select></div>\
<hr><p class="help">' + __('Then By (optional)') + '</p>\
<div class="sort-column-1"></div>\
<div><select class="sort-order-1 form-control" style="margin-top: 10px; width: 60%;">\
<option value="asc">' + __('Ascending') + '</option>\
<option value="desc">' + __('Descending') + '</option>\
</select></div><hr>\
<div><button class="btn btn-primary">' + __('Update') + '</div>');
this.sort_by_select = new frappe.ui.FieldSelect({
parent: $(this.sort_dialog.body).find('.sort-column'),
doctype: this.doctype
});
this.sort_by_select.$select.css('width', '60%');
this.sort_order_select = $(this.sort_dialog.body).find('.sort-order');
this.sort_by_next_select = new frappe.ui.FieldSelect({
parent: $(this.sort_dialog.body).find('.sort-column-1'),
doctype: this.doctype,
with_blank: true
});
this.sort_by_next_select.$select.css('width', '60%');
this.sort_order_next_select = $(this.sort_dialog.body).find('.sort-order-1');
this.sort_by_select.set_value(this.doctype, 'modified');
this.sort_order_select.val('desc');
this.sort_by_next_select.clear();
this.sort_order_next_select.val('desc');
this.page.add_inner_button(__('Sort Order'), function () {
me.sort_dialog.show();
});
$(this.sort_dialog.body).find('.btn-primary').click(function () {
me.sort_dialog.hide();
me.run();
});
},
make_export: function make_export() {
var me = this;
if (!frappe.model.can_export(this.doctype)) {
return;
}
var export_btn = this.page.add_menu_item(__('Export'), function () {
var args = me.get_args();
var selected_items = me.get_checked_items();
frappe.prompt({ fieldtype: "Select", label: __("Select File Type"), fieldname: "file_format_type",
options: "Excel\nCSV", default: "Excel", reqd: 1 }, function (data) {
args.cmd = 'frappe.desk.reportview.export_query';
args.file_format_type = data.file_format_type;
if (me.add_totals_row) {
args.add_totals_row = 1;
}
if (selected_items.length >= 1) {
args.selected_items = $.map(selected_items, function (d) {
return d.name;
});
}
open_url_post(frappe.request.url, args);
}, __("Export Report: {0}", [__(me.doctype)]), __("Download"));
}, true);
},
make_save: function make_save() {
var me = this;
if (frappe.user.is_report_manager()) {
this.page.add_menu_item(__('Save'), function () {
me.save_report('save');
}, true);
this.page.add_menu_item(__('Save As'), function () {
me.save_report('save_as');
}, true);
}
},
save_report: function save_report(save_type) {
var me = this;
var _save_report = function _save_report(name) {
return frappe.call({
method: 'frappe.desk.reportview.save_report',
args: {
name: name,
doctype: me.doctype,
json: JSON.stringify({
filters: me.filter_list.get_filters(),
columns: me.columns,
sort_by: me.sort_by_select.val(),
sort_order: me.sort_order_select.val(),
sort_by_next: me.sort_by_next_select.val(),
sort_order_next: me.sort_order_next_select.val(),
add_totals_row: me.add_totals_row
})
},
callback: function callback(r) {
if (r.exc) {
frappe.msgprint(__("Report was not saved (there were errors)"));
return;
}
if (r.message != me.docname) frappe.set_route('Report', me.doctype, r.message);
}
});
};
if (me.docname && save_type == "save") {
_save_report(me.docname);
} else {
frappe.prompt({ fieldname: 'name', label: __('New Report name'), reqd: 1, fieldtype: 'Data' }, function (data) {
_save_report(data.name);
}, __('Save As'));
}
},
make_delete: function make_delete() {
var me = this;
if (this.can_delete) {
$(this.parent).on("click", "input[type='checkbox'][data-row]", function () {
me.data[$(this).attr("data-row")].selected = this.checked ? true : false;
});
this.page.add_menu_item(__("Delete"), function () {
var delete_list = $.map(me.get_checked_items(), function (d) {
return d.name;
});
if (!delete_list.length) return;
if (frappe.confirm(__("This is PERMANENT action and you cannot undo. Continue?"), function () {
return frappe.call({
method: 'frappe.desk.reportview.delete_items',
args: {
items: delete_list,
doctype: me.doctype
},
callback: function callback() {
me.refresh();
}
});
})) ;
}, true);
}
},
make_user_permissions: function make_user_permissions() {
var me = this;
if (this.docname && frappe.model.can_set_user_permissions("Report")) {
this.page.add_menu_item(__("User Permissions"), function () {
frappe.route_options = {
doctype: "Report",
name: me.docname
};
frappe.set_route('List', 'User Permission');
}, true);
}
},
setup_listview_settings: function setup_listview_settings() {
if (frappe.listview_settings[this.doctype] && frappe.listview_settings[this.doctype].onload) {
frappe.listview_settings[this.doctype].onload(this);
}
},
get_checked_items: function get_checked_items() {
var me = this;
var selected_records = [];
$.each(me.data, function (i, d) {
if (d.selected && d.name) {
selected_records.push(d);
}
});
return selected_records;
}
});
frappe.ui.ColumnPicker = Class.extend({
init: function init(list) {
this.list = list;
this.doctype = list.doctype;
},
clear: function clear() {
this.columns = [];
$(this.dialog.body).html('<div class="text-muted">' + __("Drag to sort columns") + '</div>\
<div class="column-list"></div>\
<div><button class="btn btn-default btn-sm btn-add">' + __("Add Column") + '</button></div>');
},
show: function show(columns) {
var me = this;
if (!this.dialog) {
this.dialog = new frappe.ui.Dialog({
title: __("Pick Columns"),
width: '400',
primary_action_label: __("Update"),
primary_action: function primary_action() {
me.update_column_selection();
}
});
this.dialog.$wrapper.addClass("column-picker-dialog");
}
this.clear();
this.column_list = $(this.dialog.body).find('.column-list');
$.each(columns, function (i, c) {
me.add_column(c);
});
new Sortable(this.column_list.get(0), {
filter: 'input',
draggable: '.column-list-item',
chosenClass: 'sortable-chosen',
dragClass: 'sortable-chosen',
onUpdate: function onUpdate(event) {
me.columns = [];
$.each($(me.dialog.body).find('.column-list .column-list-item'), function (i, ele) {
me.columns.push($(ele).data("fieldselect"));
});
}
});
$(this.dialog.body).find('.btn-add').click(function () {
me.add_column(['name']);
});
this.dialog.show();
},
add_column: function add_column(c) {
if (!c) return;
var me = this;
var w = $('<div class="column-list-item"><div class="row">\
<div class="col-xs-1">\
<i class="fa fa-sort text-muted"></i></div>\
<div class="col-xs-10"></div>\
<div class="col-xs-1"><a class="close">&times;</a></div>\
</div></div>').appendTo(this.column_list);
var fieldselect = new frappe.ui.FieldSelect({ parent: w.find('.col-xs-10'), doctype: this.doctype });
fieldselect.val((c[1] || this.doctype) + "." + c[0]);
w.data("fieldselect", fieldselect);
w.find('.close').data("fieldselect", fieldselect).click(function () {
delete me.columns[me.columns.indexOf($(this).data('fieldselect'))];
$(this).parents('.column-list-item').remove();
});
this.columns.push(fieldselect);
},
update_column_selection: function update_column_selection() {
this.dialog.hide();
var columns = $.map(this.columns, function (v) {
return v && v.selected_fieldname && v.selected_doctype ? [[v.selected_fieldname, v.selected_doctype]] : null;
});
this.list.set_columns(columns);
this.list.run();
}
});frappe.templates['reportview_footer'] = '<div class="row"> <div class="col-md-6"> {% if has_child_column %} <div class="checkbox"> <label> <input type="checkbox" class="show-all-data" style="margin-top: 2px" {{ show_all_data ? "checked" : "" }}> {{ __("Show all data") }} </label> </div> {% endif %} </div> {% if can_write %} <div class="col-md-6 text-right"><p class="text-muted"> {{ __("Tip: Double click cell to edit") }}</p></div> {% endif %} </div>';
frappe.provide("frappe.views");
frappe.provide("frappe.query_reports");
frappe.standard_pages["query-report"] = function () {
var wrapper = frappe.container.add_page('query-report');
frappe.ui.make_app_page({
parent: wrapper,
title: __('Query Report'),
single_column: true
});
frappe.query_report = new frappe.views.QueryReport({
parent: wrapper
});
$(wrapper).bind("show", function () {
frappe.query_report.load();
});
};
frappe.views.QueryReport = Class.extend({
init: function init(opts) {
$.extend(this, opts);
this.flags = {};
this.page = this.parent.page;
this.parent.query_report = this;
this.make();
},
slickgrid_options: {
enableColumnReorder: false,
showHeaderRow: true,
headerRowHeight: 30,
explicitInitialization: true,
multiColumnSort: true
},
make: function make() {
var me = this;
this.wrapper = $("<div>").appendTo(this.page.main);
$('<div class="waiting-area" style="display: none;"></div>\
<div class="no-report-area msg-box no-border" style="display: none;"></div>\
<div class="chart_area" style="border-bottom: 1px solid #d1d8dd; padding: 0px 5%"></div>\
<div class="results" style="display: none;">\
<div class="result-area" style="height:400px;"></div>\
<button class="btn btn-secondary btn-default btn-xs expand-all hidden" style="margin: 10px;">' + __('Expand All') + '</button>\
<button class="btn btn-secondary btn-default btn-xs collapse-all hidden" style="margin: 10px; margin-left: 0px;">' + __('Collapse All') + '</button>\
<p class="help-msg alert alert-warning text-center" style="margin: 15px; margin-top: 0px;"></p>\
<p class="msg-box small">\
' + __('For comparative filters, start with') + ' ">" or "<" or "!", e.g. >5 or >01-02-2012 or !0\
<br>' + __('For ranges') + ' (' + __('values and dates') + ') use ":", \
e.g. "5:10" (' + __("to filter values between 5 & 10") + ')</p>\
</div>').appendTo(this.wrapper);
this.wrapper.find(".expand-all").on("click", function () {
me.toggle_all(false);
});
this.wrapper.find(".collapse-all").on("click", function () {
me.toggle_all(true);
});
this.chart_area = this.wrapper.find(".chart_area");
this.make_toolbar();
},
toggle_expand_collapse_buttons: function toggle_expand_collapse_buttons(show) {
this.wrapper.find(".expand-all, .collapse-all").toggleClass('hidden', !!!show);
},
make_toolbar: function make_toolbar() {
var me = this;
this.page.set_secondary_action(__('Refresh'), function () {
me.refresh();
});
this.page.add_menu_item(__('Edit'), function () {
if (!frappe.user.is_report_manager()) {
frappe.msgprint(__("You are not allowed to create / edit reports"));
return false;
}
frappe.set_route("Form", "Report", me.report_name);
}, true);
this.page.add_menu_item(__("Print"), function () {
frappe.ui.get_print_settings(false, function (print_settings) {
me.print_settings = print_settings;
me.print_report();
}, me.report_doc.letter_head);
}, true);
this.page.add_menu_item(__("PDF"), function () {
frappe.ui.get_print_settings(true, function (print_settings) {
me.print_settings = print_settings;
me.pdf_report();
}, me.report_doc.letter_head);
}, true);
this.page.add_menu_item(__('Export'), function () {
me.make_export();
}, true);
this.page.add_menu_item(__("Setup Auto Email"), function () {
frappe.set_route('List', 'Auto Email Report', { 'report': me.report_name });
}, true);
if (frappe.model.can_set_user_permissions("Report")) {
this.page.add_menu_item(__("User Permissions"), function () {
frappe.route_options = {
doctype: "Report",
name: me.report_name
};
frappe.set_route('List', 'User Permission');
}, true);
}
this.page.add_menu_item(__("Add to Desktop"), function () {
frappe.add_to_desktop(me.report_name, null, me.report_name);
}, true);
},
load: function load() {
var route = frappe.get_route();
var me = this;
if (route[1]) {
if (me.report_name != route[1] || frappe.route_options) {
me.report_name = route[1];
this.wrapper.find(".no-report-area").toggle(false);
me.page.set_title(__(me.report_name));
frappe.model.with_doc("Report", me.report_name, function () {
me.report_doc = frappe.get_doc("Report", me.report_name);
frappe.model.with_doctype(me.report_doc.ref_doctype, function () {
var module = locals.DocType[me.report_doc.ref_doctype].module;
frappe.breadcrumbs.add(module);
if (!frappe.query_reports[me.report_name]) {
return frappe.call({
method: "frappe.desk.query_report.get_script",
args: {
report_name: me.report_name
},
callback: function callback(r) {
frappe.dom.eval(r.message.script || "");
frappe.after_ajax(function () {
var report_settings = frappe.query_reports[me.report_name];
me.html_format = r.message.html_format;
report_settings["html_format"] = r.message.html_format;
me.setup_report();
});
}
});
} else {
me.setup_report();
}
});
});
}
} else {
var msg = __("No Report Loaded. Please use query-report/[Report Name] to run a report.");
this.wrapper.find(".no-report-area").html(msg).toggle(true);
}
},
setup_report: function setup_report() {
var me = this;
this.page.set_title(__(this.report_name));
this.page.clear_inner_toolbar();
this.setup_filters();
this.chart_area.toggle(false);
this.toggle_expand_collapse_buttons(false);
this.is_tree_report = false;
var report_settings = frappe.query_reports[this.report_name];
$.when(function () {
if (report_settings.onload) {
return report_settings.onload(me);
}
}()).then(function () {
me.refresh();
});
},
print_report: function print_report() {
if (!frappe.model.can_print(this.report_doc.ref_doctype)) {
frappe.msgprint(__("You are not allowed to print this report"));
return false;
}
if (this.html_format) {
var content = frappe.render(this.html_format, {
data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
filters: this.get_values(),
report: this
});
frappe.render_grid({
content: content,
title: __(this.report_name),
print_settings: this.print_settings
});
} else {
frappe.render_grid({
grid: this.grid,
report: this,
title: __(this.report_name),
print_settings: this.print_settings
});
}
},
pdf_report: function pdf_report() {
var me = this;
var base_url = frappe.urllib.get_base_url();
var print_css = frappe.boot.print_css;
if (!frappe.model.can_print(this.report_doc.ref_doctype)) {
frappe.msgprint(__("You are not allowed to make PDF for this report"));
return false;
}
var orientation = this.print_settings.orientation;
var landscape = orientation == "Landscape" ? true : false;
var columns = this.grid.getColumns();
if (this.html_format) {
var content = frappe.render(this.html_format, {
data: frappe.slickgrid_tools.get_filtered_items(this.dataView),
filters: this.get_values(),
report: this
});
var html = frappe.render_template("print_template", {
content: content,
title: __(this.report_name),
base_url: base_url,
print_css: print_css,
print_settings: this.print_settings,
landscape: landscape,
columns: columns
});
} else {
var visible_idx = frappe.slickgrid_tools.get_view_data(this.columns, this.dataView).map(function (row) {
return row[0];
}).filter(function (idx) {
return idx !== 'Sr No';
});
var data = this.grid.getData().getItems();
data = data.filter(function (d) {
return visible_idx.includes(d._id);
});
var content = frappe.render_template("print_grid", {
columns: columns,
data: data,
title: __(this.report_name)
});
var html = frappe.render_template("print_template", {
content: content,
title: __(this.report_name),
base_url: base_url,
print_css: print_css,
print_settings: this.print_settings,
landscape: landscape,
columns: columns
});
}
var orientation = this.print_settings.orientation;
this.open_pdf_report(html, orientation);
},
open_pdf_report: function open_pdf_report(html, orientation) {
var formData = new FormData();
formData.append("html", html);
formData.append("orientation", orientation);
var blob = new Blob([], { type: "text/xml" });
formData.append("blob", blob);
var xhr = new XMLHttpRequest();
xhr.open("POST", '/api/method/frappe.utils.print_format.report_to_pdf');
xhr.setRequestHeader("X-Frappe-CSRF-Token", frappe.csrf_token);
xhr.responseType = "arraybuffer";
xhr.onload = function (success) {
if (this.status === 200) {
var blob = new Blob([success.currentTarget.response], { type: "application/pdf" });
var objectUrl = URL.createObjectURL(blob);
window.open(objectUrl);
}
};
xhr.send(formData);
},
setup_filters: function setup_filters() {
if (this.setting_filters) return;
this.clear_filters();
var me = this;
$.each(frappe.query_reports[this.report_name].filters || [], function (i, df) {
if (df.fieldtype === "Break") {
me.page.add_break();
} else {
var f = me.page.add_field(df);
$(f.wrapper).addClass("filters pull-left");
me.filters.push(f);
if (df["default"]) {
f.set_input(df["default"]);
}
if (df.fieldtype == "Check") {
$(f.wrapper).find("input[type='checkbox']");
}
if (df.get_query) f.get_query = df.get_query;
if (df.on_change) f.on_change = df.on_change;
df.onchange = function () {
if (!me.flags.filters_set) {
return;
}
if (f.on_change) {
f.on_change(me);
} else {
me.trigger_refresh();
}
};
df.ignore_link_validation = true;
}
});
var $filters = $(this.parent).find('.page-form .filters');
$(this.parent).find('.page-form').toggle($filters.length ? true : false);
this.setting_filters = true;
this.set_route_filters();
this.setting_filters = false;
this.set_filters_by_name();
this.flags.filters_set = true;
},
clear_filters: function clear_filters() {
this.filters = [];
$(this.parent).find('.page-form .filters').remove();
},
set_route_filters: function set_route_filters() {
var me = this;
if (frappe.route_options) {
$.each(this.filters || [], function (i, f) {
if (frappe.route_options[f.df.fieldname] != null) {
f.set_value(frappe.route_options[f.df.fieldname]);
}
});
}
frappe.route_options = null;
},
set_filters_by_name: function set_filters_by_name() {
frappe.query_report_filters_by_name = {};
for (var i in this.filters) {
frappe.query_report_filters_by_name[this.filters[i].df.fieldname] = this.filters[i];
}
},
refresh: function refresh() {
var me = this;
this.wrapper.find(".results").toggle(false);
try {
var filters = this.get_values(true);
} catch (e) {
return;
}
this.waiting = frappe.messages.waiting(this.wrapper.find(".waiting-area").empty().toggle(true), __("Loading Report") + "...");
this.wrapper.find(".no-report-area").toggle(false);
if (this.report_ajax) {
this.report_ajax.abort();
}
this.chart_area.toggle(false);
this.report_ajax = frappe.call({
method: "frappe.desk.query_report.run",
type: "GET",
args: {
"report_name": me.report_name,
filters: filters
},
callback: function callback(r) {
me.report_ajax = undefined;
me.make_results(r.message);
}
});
return this.report_ajax;
},
trigger_refresh: function trigger_refresh() {
var me = this;
var filters = me.get_values();
var missing = false;
$.each(me.filters, function (k, _f) {
if (_f.df.reqd && !filters[_f.df.fieldname]) {
missing = true;
return;
}
});
if (!missing) {
me.refresh();
}
},
get_values: function get_values(raise) {
var filters = {};
var mandatory_fields = [];
$.each(this.filters || [], function (i, f) {
var v = f.get_value();
if (f.df.hidden) v = f.value;
if (v === '%') v = null;
if (f.df.reqd && !v) mandatory_fields.push(f.df.label);
if (v) filters[f.df.fieldname] = v;
});
if (raise && mandatory_fields.length) {
this.chart_area.hide();
this.wrapper.find(".waiting-area").empty().toggle(false);
this.wrapper.find(".no-report-area").html(__("Please set filters")).toggle(true);
if (raise) {
console.log('filter missing: ' + mandatory_fields);
throw "Filters required";
}
}
return filters;
},
make_results: function make_results(res) {
this.wrapper.find(".waiting-area, .no-report-area").empty().toggle(false);
this.wrapper.find(".results").toggle(true);
this.make_columns(res.columns);
this.make_data(res.result, res.columns);
this.filter_hidden_columns();
this.render(res);
},
render: function render(res) {
this.columnFilters = {};
this.make_dataview();
this.id = frappe.dom.set_unique_id(this.wrapper.find(".result-area").addClass("slick-wrapper").get(0));
this.grid = new Slick.Grid("#" + this.id, this.dataView, this.columns, this.slickgrid_options);
if (!frappe.dom.is_touchscreen()) {
this.grid.setSelectionModel(new Slick.CellSelectionModel());
this.grid.registerPlugin(new Slick.CellExternalCopyManager({
dataItemColumnValueExtractor: function dataItemColumnValueExtractor(item, columnDef, value) {
return item[columnDef.field];
}
}));
}
this.setup_header_row();
this.grid.init();
this.setup_sort();
if (this.get_query_report_opts().tree) {
this.setup_tree();
}
this.set_message(res.message);
this.setup_chart(res);
this.toggle_expand_collapse_buttons(this.is_tree_report);
},
make_columns: function make_columns(columns) {
var me = this;
var formatter = this.get_formatter();
this.columns = [{ id: "_id", field: "_id", name: __("Sr No"), width: 60 }].concat($.map(columns, function (c, i) {
if ($.isPlainObject(c)) {
var df = c;
} else if (c.indexOf(":") !== -1) {
var opts = c.split(":");
var df = {
label: opts.length <= 2 ? opts[0] : opts.slice(0, opts.length - 2).join(":"),
fieldtype: opts.length <= 2 ? opts[1] : opts[opts.length - 2],
width: opts.length <= 2 ? opts[2] : opts[opts.length - 1]
};
if (df.fieldtype.indexOf("/") !== -1) {
var tmp = df.fieldtype.split("/");
df.fieldtype = tmp[0];
df.options = tmp[1];
}
df.width = cint(df.width);
} else {
var df = {
label: c,
fieldtype: "Data"
};
}
if (!df.fieldtype) df.fieldtype = "Data";
if (!cint(df.width)) df.width = 80;
var col = $.extend({}, df, {
label: df.label || df.fieldname && __(toTitle(df.fieldname.replace(/_/g, " "))) || "",
sortable: true,
df: df,
formatter: formatter
});
col.field = df.fieldname || df.label;
df.label = __(df.label);
col.name = col.id = col.label = df.label;
return col;
}));
},
filter_hidden_columns: function filter_hidden_columns() {
this.columns = $.map(this.columns, function (c, i) {
return c.hidden == 1 ? null : c;
});
},
get_query_report_opts: function get_query_report_opts() {
return frappe.query_reports[this.report_name] || {};
},
get_formatter: function get_formatter() {
var formatter = function formatter(row, cell, value, columnDef, dataContext, for_print) {
var value = frappe.format(value, columnDef.df, { for_print: for_print, always_show_decimals: true }, dataContext);
if (columnDef.df.is_tree) {
value = frappe.query_report.tree_formatter(row, cell, value, columnDef, dataContext);
}
return value;
};
var query_report_opts = this.get_query_report_opts();
if (query_report_opts.formatter) {
var default_formatter = formatter;
formatter = function formatter(row, cell, value, columnDef, dataContext) {
return query_report_opts.formatter(row, cell, value, columnDef, dataContext, default_formatter);
};
}
return formatter;
},
make_data: function make_data(result, columns) {
var me = this;
this.data = [];
for (var row_idx = 0, l = result.length; row_idx < l; row_idx++) {
var row = result[row_idx];
if ($.isPlainObject(row)) {
var newrow = row;
} else {
var newrow = {};
for (var i = 1, j = this.columns.length; i < j; i++) {
newrow[this.columns[i].field] = row[i - 1];
}
}
newrow._id = row_idx + 1;
newrow.id = newrow.name ? newrow.name : "_" + newrow._id;
this.data.push(newrow);
}
},
make_dataview: function make_dataview() {
this.dataView = new Slick.Data.DataView({ inlineFilters: true });
this.dataView.beginUpdate();
if (this.get_query_report_opts().tree) {
this.setup_item_by_name();
this.dataView.setFilter(this.tree_filter);
} else {
this.dataView.setFilter(this.inline_filter);
}
this.dataView.setItems(this.data);
this.dataView.endUpdate();
var me = this;
this.dataView.onRowCountChanged.subscribe(function (e, args) {
me.grid.updateRowCount();
me.grid.render();
});
this.dataView.onRowsChanged.subscribe(function (e, args) {
me.grid.invalidateRows(args.rows);
me.grid.render();
});
},
inline_filter: function inline_filter(item) {
var me = frappe.container.page.query_report;
for (var columnId in me.columnFilters) {
if (columnId !== undefined && me.columnFilters[columnId] !== "") {
var c = me.grid.getColumns()[me.grid.getColumnIndex(columnId)];
if (!me.compare_values(item[c.field], me.columnFilters[columnId], me.columns[me.grid.getColumnIndex(columnId)])) {
return false;
}
}
}
return true;
},
setup_item_by_name: function setup_item_by_name() {
this.item_by_name = {};
this.name_field = this.get_query_report_opts().name_field;
this.parent_field = this.get_query_report_opts().parent_field;
var initial_depth = this.get_query_report_opts().initial_depth;
for (var i = 0, l = this.data.length; i < l; i++) {
var item = this.data[i];
if (item[this.name_field]) {
this.item_by_name[item[this.name_field]] = item;
}
if (initial_depth && item.indent && item.indent >= initial_depth - 1) {
item._collapsed = true;
}
}
},
toggle_all: function toggle_all(collapse) {
var me = this;
for (var i = 0, l = this.data.length; i < l; i++) {
var item = this.data[i];
item._collapsed = collapse;
me.dataView.updateItem(item.id, item);
}
},
tree_filter: function tree_filter(item) {
var me = frappe.query_report;
if (!me.inline_filter(item)) return false;
try {
var parent_name = item[me.parent_field];
while (parent_name) {
if (!me.item_by_name[parent_name] || me.item_by_name[parent_name]._collapsed) {
return false;
}
parent_name = me.item_by_name[parent_name][me.parent_field];
}
return true;
} catch (e) {
if (e.message.indexOf("[parent_name] is undefined") !== -1) {
frappe.msgprint(__("Unable to display this tree report, due to missing data. Most likely, it is being filtered out due to permissions."));
}
throw e;
}
},
tree_formatter: function tree_formatter(row, cell, value, columnDef, dataContext) {
var me = frappe.query_report;
me.is_tree_report = true;
var $span = $("<span></span>").css("padding-left", cint(dataContext.indent) * 21 + "px").html(value);
var idx = me.dataView.getIdxById(dataContext.id);
var show_toggle = me.data[idx + 1] && me.data[idx + 1].indent > me.data[idx].indent;
if (dataContext[me.name_field] && show_toggle) {
$('<span class="toggle"></span>').addClass(dataContext._collapsed ? "expand" : "collapse").css("margin-right", "7px").prependTo($span);
}
return $span.wrap("<p></p>").parent().html();
},
compare_values: function compare_values(value, filter, columnDef) {
var invert = false;
if (filter[0] == "!") {
invert = true;
filter = filter.substr(1);
}
var out = false;
var cond = "==";
if (filter[0] == ">") {
filter = filter.substr(1);
cond = ">";
} else if (filter[0] == "<") {
filter = filter.substr(1);
cond = "<";
}
if (in_list(['Float', 'Currency', 'Int', 'Date'], columnDef.df.fieldtype)) {
if (filter.indexOf(":") == -1) {
if (columnDef.df.fieldtype == "Date") {
filter = frappe.datetime.user_to_str(filter);
}
if (in_list(["Float", "Currency", "Int"], columnDef.df.fieldtype)) {
value = flt(value);
filter = flt(filter);
}
out = eval("value" + cond + "filter");
} else {
filter = filter.split(":");
if (columnDef.df.fieldtype == "Date") {
filter[0] = frappe.datetime.user_to_str(filter[0]);
filter[1] = frappe.datetime.user_to_str(filter[1]);
}
if (in_list(["Float", "Currency", "Int"], columnDef.df.fieldtype)) {
value = flt(value);
filter[0] = flt(filter[0]);
filter[1] = flt(filter[1]);
}
out = value >= filter[0] && value <= filter[1];
}
} else {
value = value + "";
value = value.toLowerCase();
filter = filter.toLowerCase();
out = value.indexOf(filter) != -1;
}
if (invert) return !out;else return out;
},
setup_header_row: function setup_header_row() {
var me = this;
$(this.grid.getHeaderRow()).delegate(":input", "change keyup", function (e) {
var columnId = $(this).data("columnId");
if (columnId != null) {
me.columnFilters[columnId] = $.trim($(this).val());
me.dataView.refresh();
}
});
this.grid.onHeaderRowCellRendered.subscribe(function (e, args) {
$(args.node).empty();
$("<input type='text'>").data("columnId", args.column.id).val(me.columnFilters[args.column.id]).appendTo(args.node);
});
},
setup_sort: function setup_sort() {
var me = this;
this.grid.onSort.subscribe(function (e, args) {
var cols = args.sortCols;
me.data.sort(function (dataRow1, dataRow2) {
for (var i = 0, l = cols.length; i < l; i++) {
var field = cols[i].sortCol.field;
var sign = cols[i].sortAsc ? 1 : -1;
var value1 = dataRow1[field],
value2 = dataRow2[field];
var result = (value1 == value2 ? 0 : value1 > value2 ? 1 : -1) * sign;
if (result != 0) {
return result;
}
}
return 0;
});
me.dataView.beginUpdate();
me.dataView.setItems(me.data);
me.dataView.endUpdate();
me.dataView.refresh();
});
},
setup_tree: function setup_tree() {
var me = this;
this.grid.onClick.subscribe(function (e, args) {
if ($(e.target).hasClass("toggle")) {
var item = me.dataView.getItem(args.row);
if (item) {
if (!item._collapsed) {
item._collapsed = true;
} else {
item._collapsed = false;
}
me.dataView.updateItem(item.id, item);
}
e.stopImmediatePropagation();
}
});
},
make_export: function make_export() {
var me = this;
this.title = this.report_name;
if (!frappe.model.can_export(this.report_doc.ref_doctype)) {
frappe.msgprint(__("You are not allowed to export this report"));
return false;
}
frappe.prompt({ fieldtype: "Select", label: __("Select File Type"), fieldname: "file_format_type",
options: "Excel\nCSV", default: "Excel", reqd: 1 }, function (data) {
var view_data = frappe.slickgrid_tools.get_view_data(me.columns, me.dataView);
var result = view_data.map(function (row) {
return row.splice(1);
});
var visible_idx = view_data.map(function (row) {
return row[0];
}).filter(function (sr_no) {
return sr_no !== 'Sr No';
});
if (data.file_format_type == "CSV") {
frappe.tools.downloadify(result, null, me.title);
} else if (data.file_format_type == "Excel") {
try {
var filters = me.get_values(true);
} catch (e) {
return;
}
var args = {
cmd: 'frappe.desk.query_report.export_query',
report_name: me.report_name,
file_format_type: data.file_format_type,
filters: filters,
visible_idx: visible_idx
};
open_url_post(frappe.request.url, args);
}
}, __("Export Report: " + me.title), __("Download"));
return false;
},
set_message: function set_message(msg) {
if (msg) {
this.wrapper.find(".help-msg").html(msg).toggle(true);
} else {
this.wrapper.find(".help-msg").empty().toggle(false);
}
},
setup_chart: function setup_chart(res) {
this.chart_area.toggle(false);
if (this.get_query_report_opts().get_chart_data) {
var opts = this.get_query_report_opts().get_chart_data(res.columns, res.result);
} else if (res.chart) {
var opts = res.chart;
} else {
return;
}
$.extend(opts, {
wrapper: this.chart_area
});
this.chart = new frappe.ui.Chart(opts);
if (this.chart && opts.data && opts.data.rows && opts.data.rows.length) {
this.chart_area.toggle(true);
}
}
});
frappe.provide("frappe.report_dump");
$.extend(frappe.report_dump, {
data: {},
last_modified: {},
with_data: function with_data(doctypes, _callback) {
var pre_loaded = Object.keys(frappe.report_dump.last_modified);
return frappe.call({
method: "frappe.desk.report_dump.get_data",
type: "GET",
args: {
doctypes: doctypes,
last_modified: frappe.report_dump.last_modified
},
freeze: true,
callback: function callback(r) {
$.each(r.message, function (doctype, doctype_data) {
frappe.report_dump.set_data(doctype, doctype_data);
});
$.each(r.message, function (doctype, doctype_data) {
if (!in_list(pre_loaded, doctype)) {
if (doctype_data.links) {
$.each(frappe.report_dump.data[doctype], function (row_idx, row) {
$.each(doctype_data.links, function (link_key, link) {
if (frappe.report_dump.data[link[0]][row[link_key]]) {
row[link_key] = frappe.report_dump.data[link[0]][row[link_key]][link[1]];
} else {
row[link_key] = null;
}
});
});
}
}
});
_callback();
}
});
},
set_data: function set_data(doctype, doctype_data) {
var data = [];
var replace_dict = {};
var make_row = function make_row(d) {
var row = {};
$.each(doctype_data.columns, function (idx, col) {
row[col] = d[idx];
});
row.id = row.name;
row.doctype = doctype;
return row;
};
if (frappe.report_dump.last_modified[doctype]) {
$.each(doctype_data.data, function (i, d) {
var row = make_row(d);
replace_dict[row.name] = row;
});
$.each(frappe.report_dump.data[doctype], function (i, d) {
if (replace_dict[d.name]) {
data.push(replace_dict[d.name]);
delete replace_dict[d.name];
} else if (doctype_data.modified_names.indexOf(d.name) !== -1) {} else {
data.push(d);
}
});
$.each(replace_dict, function (name, d) {
data.push(d);
});
} else {
$.each(doctype_data.data, function (i, d) {
data.push(make_row(d));
});
}
frappe.report_dump.last_modified[doctype] = doctype_data.last_modified;
frappe.report_dump.data[doctype] = data;
}
});
frappe.provide("frappe.views");
frappe.views.GridReport = Class.extend({
init: function init(opts) {
this.filter_inputs = {};
this.preset_checks = [];
this.tree_grid = { show: false };
var me = this;
$.extend(this, opts);
this.wrapper = $('<div class="grid-report"></div>').appendTo(this.page.main);
this.page.main.find(".page").css({ "padding-top": "0px" });
if (this.filters) {
this.make_filters();
}
this.make_waiting();
this.get_data_and_refresh();
},
bind_show: function bind_show() {
var me = this;
$(this.page).bind('show', function () {
frappe.cur_grid_report = me;
me.get_data_and_refresh();
});
},
get_data_and_refresh: function get_data_and_refresh() {
var me = this;
this.get_data(function () {
me.apply_filters_from_route();
me.refresh();
});
},
get_data: function get_data(callback) {
var me = this;
frappe.report_dump.with_data(this.doctypes, function () {
if (!me.setup_filters_done) {
me.setup_filters();
me.setup_filters_done = true;
}
callback();
});
},
setup_filters: function setup_filters() {
var me = this;
$.each(me.filter_inputs, function (i, v) {
var opts = v.get(0).opts;
if (opts.fieldtype == "Select" && in_list(me.doctypes, opts.link)) {
$(v).add_options($.map(frappe.report_dump.data[opts.link], function (d) {
return d.name;
}));
} else if (opts.fieldtype == "Link" && in_list(me.doctypes, opts.link)) {
opts.list = $.map(frappe.report_dump.data[opts.link], function (d) {
return d.name;
});
me.set_autocomplete(v, opts.list);
}
});
this.page.set_primary_action(__("Refresh"), function () {
me.get_data(function () {
me.refresh();
});
});
if (this.filter_inputs) {
this.page.add_menu_item(__("Reset Filters"), function () {
me.init_filter_values();
me.refresh();
}, true);
}
this.page.add_menu_item(__("Print"), function () {
frappe.ui.get_print_settings(false, function (print_settings) {
frappe.render_grid({ grid: me.grid, title: me.page.title, print_settings: print_settings });
});
}, true);
this.filter_inputs.range && this.filter_inputs.range.on("change", function () {
me.refresh();
});
if (this.setup_chart_check) this.setup_chart_check();
},
set_filter: function set_filter(key, value) {
var filters = this.filter_inputs[key];
if (filters) {
var opts = filters.get(0).opts;
if (opts.fieldtype === "Check") {
filters.prop("checked", cint(value) ? true : false);
}if (opts.fieldtype == "Date") {
filters.val(frappe.datetime.str_to_user(value));
} else {
filters.val(value);
}
} else {
frappe.msgprint(__("Invalid Filter: {0}", [key]));
}
},
set_autocomplete: function set_autocomplete($filter, list) {
var me = this;
new Awesomplete($filter.get(0), {
list: list
});
$filter.on("awesomplete-select", function (e) {
var value = e.originalEvent.text.value;
$filter.val(value);
me.refresh();
});
},
init_filter_values: function init_filter_values() {
var me = this;
$.each(this.filter_inputs, function (key, filter) {
var opts = filter.get(0).opts;
if (frappe.sys_defaults[key]) {
filter.val(frappe.sys_defaults[key]);
} else if (opts.fieldtype == 'Select') {
filter.get(0).selectedIndex = 0;
} else if (opts.fieldtype == 'Data') {
filter.val("");
} else if (opts.fieldtype == "Link") {
filter.val("");
}
});
this.set_default_values();
},
set_default_values: function set_default_values() {
var values = {
from_date: frappe.datetime.str_to_user(frappe.sys_defaults.year_start_date),
to_date: frappe.datetime.str_to_user(frappe.sys_defaults.year_end_date)
};
var me = this;
$.each(values, function (i, v) {
if (me.filter_inputs[i] && !me.filter_inputs[i].val()) me.filter_inputs[i].val(v);
});
},
make_filters: function make_filters() {
var me = this;
$.each(this.filters, function (i, v) {
v.fieldname = v.fieldname || v.label.replace(/ /g, '_').toLowerCase();
var input = null;
if (v.fieldtype == 'Select') {
input = me.page.add_select(v.label, v.options || [v.default_value]);
} else if (v.fieldtype == "Link") {
input = me.page.add_data(v.label);
new Awesomplete(input.get(0), {
list: v.list || []
});
} else if (v.fieldtype === 'Button' && v.label === __("Refresh")) {
input = me.page.set_primary_action(v.label, null, v.icon);
} else if (v.fieldtype === 'Button') {
input = me.page.add_menu_item(v.label, null, true);
} else if (v.fieldtype === 'Date') {
input = me.page.add_date(v.label);
} else if (v.fieldtype === 'Label') {
input = me.page.add_label(v.label);
} else if (v.fieldtype === 'Data') {
input = me.page.add_data(v.label);
} else if (v.fieldtype === 'Check') {
input = me.page.add_check(v.label);
}
if (input) {
input && (input.get(0).opts = v);
if (v.cssClass) {
input.addClass(v.cssClass);
}
input.keypress(function (e) {
if (e.which == 13) {
me.refresh();
}
});
}
me.filter_inputs[v.fieldname] = input;
});
},
make_waiting: function make_waiting() {
this.waiting = frappe.messages.waiting(this.wrapper, __("Loading Report") + "...");
},
load_filter_values: function load_filter_values() {
var me = this;
$.each(this.filter_inputs, function (i, f) {
var opts = f.get(0).opts;
if (opts.fieldtype == 'Check') {
me[opts.fieldname] = f.prop('checked') ? 1 : 0;
} else if (opts.fieldtype != 'Button') {
me[opts.fieldname] = f.val();
if (opts.fieldtype == "Date") {
me[opts.fieldname] = frappe.datetime.user_to_str(me[opts.fieldname]);
} else if (opts.fieldtype == "Select") {
me[opts.fieldname + '_default'] = opts.default_value;
}
}
});
if (this.filter_inputs.from_date && this.filter_inputs.to_date && this.to_date < this.from_date) {
frappe.msgprint(__("From Date must be before To Date"));
return;
}
},
make_name_map: function make_name_map(data, key) {
var map = {};
key = key || "name";
$.each(data, function (i, v) {
map[v[key]] = v;
});
return map;
},
reset_item_values: function reset_item_values(item) {
var me = this;
$.each(this.columns, function (i, col) {
if (col.formatter == me.currency_formatter) {
item[col.id] = 0.0;
}
});
},
round_item_values: function round_item_values(item) {
var me = this;
$.each(this.columns, function (i, col) {
if (col.formatter == me.currency_formatter) {
item[col.id] = flt(item[col.id], frappe.defaults.get_default("float_precision") || 3);
}
});
},
round_off_data: function round_off_data() {
var me = this;
$.each(this.data, function (i, d) {
me.round_item_values(d);
});
},
refresh: function refresh() {
this.waiting.toggle(false);
if (!this.grid_wrapper) this.make();
this.show_zero = $('.show-zero input:checked').length;
this.load_filter_values();
this.setup_columns();
this.setup_dataview_columns();
this.apply_link_formatters();
this.prepare_data();
this.round_off_data();
this.prepare_data_view();
frappe.show_alert("Updated", 2);
this.render();
this.setup_chart && this.setup_chart();
},
setup_dataview_columns: function setup_dataview_columns() {
this.dataview_columns = $.map(this.columns, function (col) {
return !col.hidden ? col : null;
});
},
make: function make() {
var me = this;
this.chart_area = $('<div class="chart" style="padding-bottom: 1px"></div>').appendTo(this.wrapper);
this.page.add_menu_item(__("Export"), function () {
return me.export();
}, true);
this.grid_wrapper = $("<div style='height: 500px; border: 1px solid #aaa; \
background-color: #eee; '>").appendTo(this.wrapper);
this.id = frappe.dom.set_unique_id(this.grid_wrapper.get(0));
$('<div class="checkbox show-zero">\
<label><input type="checkbox"> ' + __('Show rows with zero values') + '</label></div>').appendTo(this.wrapper);
this.bind_show();
frappe.cur_grid_report = this;
$(this.wrapper).trigger('make');
},
apply_filters_from_route: function apply_filters_from_route() {
var me = this;
if (frappe.route_options) {
$.each(frappe.route_options, function (key, value) {
me.set_filter(key, value);
});
frappe.route_options = null;
} else {
this.init_filter_values();
}
this.set_default_values();
$(this.wrapper).trigger('apply_filters_from_route');
},
options: {
editable: false,
enableColumnReorder: false
},
render: function render() {
this.grid = new Slick.Grid("#" + this.id, this.dataView, this.dataview_columns, this.options);
var me = this;
if (!frappe.dom.is_touchscreen()) {
this.grid.setSelectionModel(new Slick.CellSelectionModel());
this.grid.registerPlugin(new Slick.CellExternalCopyManager({
dataItemColumnValueExtractor: function dataItemColumnValueExtractor(item, columnDef, value) {
return item[columnDef.field];
}
}));
}
this.dataView.onRowsChanged.subscribe(function (e, args) {
me.grid.invalidateRows(args.rows);
me.grid.render();
});
this.dataView.onRowCountChanged.subscribe(function (e, args) {
me.grid.updateRowCount();
me.grid.render();
});
this.tree_grid.show && this.add_tree_grid_events();
},
prepare_data_view: function prepare_data_view() {
this.dataView = new Slick.Data.DataView({ inlineFilters: true });
this.dataView.beginUpdate();
this.dataView.setItems(this.data);
if (this.dataview_filter) this.dataView.setFilter(this.dataview_filter);
if (this.tree_grid.show) this.dataView.setFilter(this.tree_dataview_filter);
this.dataView.endUpdate();
},
export: function _export() {
frappe.tools.downloadify(frappe.slickgrid_tools.get_view_data(this.columns, this.dataView), ["Report Manager", "System Manager"], this.title);
return false;
},
apply_filters: function apply_filters(item) {
var filters = this.filter_inputs;
if (item._show) return true;
for (var i in filters) {
if (!this.apply_filter(item, i)) {
return false;
}
}
return true;
},
apply_filter: function apply_filter(item, fieldname) {
var filter = this.filter_inputs[fieldname].get(0);
if (filter.opts.filter) {
if (!filter.opts.filter(this[filter.opts.fieldname], item, filter.opts, this)) {
return false;
}
}
return true;
},
apply_zero_filter: function apply_zero_filter(val, item, opts, me) {
if (!me.show_zero) {
for (var i = 0, j = me.columns.length; i < j; i++) {
var col = me.columns[i];
if (col.formatter == me.currency_formatter && !col.hidden) {
if (flt(item[col.field]) > 0.001 || flt(item[col.field]) < -0.001) {
return true;
}
}
}
return false;
}
return true;
},
show_zero_check: function show_zero_check() {
var me = this;
this.wrapper.bind('make', function () {
me.wrapper.find('.show-zero').toggle(true).find('input').click(function () {
me.refresh();
});
});
},
is_default: function is_default(fieldname) {
return this[fieldname] == this[fieldname + "_default"];
},
date_formatter: function date_formatter(row, cell, value, columnDef, dataContext) {
return frappe.datetime.str_to_user(value);
},
currency_formatter: function currency_formatter(row, cell, value, columnDef, dataContext) {
return repl('<div style="text-align: right; %(_style)s">%(value)s</div>', {
_style: dataContext._style || "",
value: value == null || value === "" ? "" : format_number(value)
});
},
text_formatter: function text_formatter(row, cell, value, columnDef, dataContext) {
return repl('<span style="%(_style)s" title="%(esc_value)s">%(value)s</span>', {
_style: dataContext._style || "",
esc_value: cstr(value).replace(/"/g, '\"'),
value: cstr(value)
});
},
check_formatter: function check_formatter(row, cell, value, columnDef, dataContext) {
return repl('<input type="checkbox" data-id="%(id)s" \
class="chart-check" %(checked)s>', {
"id": dataContext.id,
"checked": dataContext.checked ? 'checked="checked"' : ""
});
},
apply_link_formatters: function apply_link_formatters() {
var me = this;
$.each(this.dataview_columns, function (i, col) {
if (col.link_formatter) {
col.formatter = function (row, cell, value, columnDef, dataContext, for_print) {
if (!value) return "";
if (for_print) {
return value;
}
var me = frappe.cur_grid_report;
if (dataContext._show) {
return repl('<span style="%(_style)s">%(value)s</span>', {
_style: dataContext._style || "",
value: value
});
}
var link_formatter = me.dataview_columns[cell].link_formatter;
if (link_formatter.filter_input) {
var html = repl('<a href="#" \
onclick="frappe.cur_grid_report.set_filter(\'%(col_name)s\', \'%(value)s\'); \
frappe.cur_grid_report.refresh(); return false;">\
%(value)s</a>', {
value: value,
col_name: link_formatter.filter_input,
page_name: frappe.container.page.page_name
});
} else {
var html = value;
}
if (link_formatter.open_btn) {
var doctype = link_formatter.doctype ? eval(link_formatter.doctype) : dataContext.doctype;
html += me.get_link_open_icon(doctype, value);
}
return html;
};
}
});
},
get_link_open_icon: function get_link_open_icon(doctype, name) {
return repl(' <a href="#Form/%(doctype)s/%(name)s">\
<i class="fa fa-share" style="cursor: pointer;"></i></a>', {
doctype: doctype,
name: encodeURIComponent(name)
});
},
make_date_range_columns: function make_date_range_columns() {
this.columns = [];
var me = this;
var range = this.filter_inputs.range.val();
this.from_date = frappe.datetime.user_to_str(this.filter_inputs.from_date.val());
this.to_date = frappe.datetime.user_to_str(this.filter_inputs.to_date.val());
var date_diff = frappe.datetime.get_diff(this.to_date, this.from_date);
me.column_map = {};
me.last_date = null;
var add_column = function add_column(date) {
me.columns.push({
id: date,
name: frappe.datetime.str_to_user(date),
field: date,
formatter: me.currency_formatter,
width: 100
});
};
var build_columns = function build_columns(condition) {
for (var i = 0; i <= date_diff; i++) {
var date = frappe.datetime.add_days(me.from_date, i);
if (!condition) condition = function condition() {
return true;
};
if (condition(date)) add_column(date);
me.last_date = date;
if (me.columns.length) {
me.column_map[date] = me.columns[me.columns.length - 1];
}
}
};
if (range == 'Daily') {
build_columns();
} else if (range == 'Weekly') {
build_columns(function (date) {
if (!me.last_date) return true;
return !(frappe.datetime.get_diff(date, me.from_date) % 7);
});
} else if (range == 'Monthly') {
build_columns(function (date) {
if (!me.last_date) return true;
return frappe.datetime.str_to_obj(me.last_date).getMonth() != frappe.datetime.str_to_obj(date).getMonth();
});
} else if (range == 'Quarterly') {
build_columns(function (date) {
if (!me.last_date) return true;
return frappe.datetime.str_to_obj(date).getDate() == 1 && in_list([0, 3, 6, 9], frappe.datetime.str_to_obj(date).getMonth());
});
} else if (range == 'Yearly') {
build_columns(function (date) {
if (!me.last_date) return true;
return $.map(frappe.report_dump.data['Fiscal Year'], function (v) {
return date == v.year_start_date ? true : null;
}).length;
});
}
$.each(this.columns, function (i, col) {
col.name = me.columns[i + 1] ? frappe.datetime.str_to_user(frappe.datetime.add_days(me.columns[i + 1].id, -1)) : frappe.datetime.str_to_user(me.to_date);
});
},
trigger_refresh_on_change: function trigger_refresh_on_change(filters) {
var me = this;
$.each(filters, function (i, f) {
me.filter_inputs[f] && me.filter_inputs[f].on("change", function () {
me.refresh();
});
});
}
});
frappe.views.GridReportWithPlot = frappe.views.GridReport.extend({
setup_chart: function setup_chart() {
var me = this;
if (in_list(["Daily", "Weekly"], this.filter_inputs.range.val())) {
this.chart_area.toggle(false);
return;
}
var chart_data = this.get_chart_data ? this.get_chart_data() : null;
this.chart = new frappe.ui.Chart({
wrapper: this.chart_area,
data: chart_data,
x_type: 'timeseries'
});
},
setup_chart_check: function setup_chart_check() {
var me = this;
me.wrapper.bind('make', function () {
me.wrapper.on("click", ".chart-check", function () {
var checked = $(this).prop("checked");
var id = $(this).attr("data-id");
if (me.item_by_name) {
if (me.item_by_name[id]) {
me.item_by_name[id].checked = checked ? true : false;
}
} else {
$.each(me.data, function (i, d) {
if (d.id == id) d.checked = checked;
});
}
me.setup_chart();
});
});
},
get_chart_data: function get_chart_data() {
var me = this;
var plottable_cols = [];
$.each(me.columns, function (idx, col) {
if (col.formatter == me.currency_formatter && !col.hidden && col.plot !== false) {
plottable_cols.push(col.field);
}
});
var data = {
x: 'x',
'columns': [['x'].concat(plottable_cols)]
};
$.each(this.data, function (i, item) {
if (item.checked) {
var data_points = [item.name];
$.each(plottable_cols, function (idx, col) {
data_points.push(item[col]);
});
data["columns"].push(data_points);
}
});
return data;
}
});
frappe.views.TreeGridReport = frappe.views.GridReportWithPlot.extend({
make_transaction_list: function make_transaction_list(parent_doctype, doctype) {
var me = this;
var tmap = {};
$.each(frappe.report_dump.data[doctype], function (i, v) {
if (!tmap[v.parent]) tmap[v.parent] = [];
tmap[v.parent].push(v);
});
if (!this.tl) this.tl = {};
if (!this.tl[parent_doctype]) this.tl[parent_doctype] = [];
$.each(frappe.report_dump.data[parent_doctype], function (i, parent) {
if (tmap[parent.name]) {
$.each(tmap[parent.name], function (i, d) {
me.tl[parent_doctype].push($.extend(copy_dict(parent), d));
});
}
});
},
add_tree_grid_events: function add_tree_grid_events() {
var me = this;
this.grid.onClick.subscribe(function (e, args) {
if ($(e.target).hasClass("toggle")) {
var item = me.dataView.getItem(args.row);
if (item) {
if (!item._collapsed) {
item._collapsed = true;
} else {
item._collapsed = false;
}
me.dataView.updateItem(item.id, item);
}
e.stopImmediatePropagation();
}
});
},
tree_formatter: function tree_formatter(row, cell, value, columnDef, dataContext) {
var me = frappe.cur_grid_report;
var data = me.data;
var spacer = "<span style='display:inline-block;height:1px;width:" + 15 * dataContext["indent"] + "px'></span>";
var idx = me.dataView.getIdxById(dataContext.id);
var link = me.tree_grid.formatter(dataContext);
if (dataContext.doctype) {
link += me.get_link_open_icon(dataContext.doctype, dataContext.name);
}
if (data[idx + 1] && data[idx + 1].indent > data[idx].indent) {
if (dataContext._collapsed) {
return spacer + " <span class='toggle expand'></span>&nbsp;" + link;
} else {
return spacer + " <span class='toggle collapse'></span>&nbsp;" + link;
}
} else {
return spacer + " <span class='toggle'></span>&nbsp;" + link;
}
},
tree_dataview_filter: function tree_dataview_filter(item) {
var me = frappe.cur_grid_report;
if (!me.apply_filters(item)) return false;
var parent = item[me.tree_grid.parent_field];
while (parent) {
if (me.item_by_name[parent]._collapsed) {
return false;
}
parent = me.parent_map[parent];
}
return true;
},
prepare_tree: function prepare_tree(item_dt, group_dt) {
var group_data = frappe.report_dump.data[group_dt];
var item_data = frappe.report_dump.data[item_dt];
var me = this;
var item_group_map = {};
var group_ids = $.map(group_data, function (v) {
return v.id;
});
$.each(item_data, function (i, item) {
var parent = item[me.tree_grid.parent_field];
if (!item_group_map[parent]) item_group_map[parent] = [];
if (group_ids.indexOf(item.name) == -1) {
item_group_map[parent].push(item);
} else {
frappe.msgprint(__("Ignoring Item {0}, because a group exists with the same name!", [item.name.bold()]));
}
});
var items = [];
$.each(group_data, function (i, group) {
group.is_group = true;
items.push(group);
items = items.concat(item_group_map[group.name] || []);
});
return items;
},
set_indent: function set_indent() {
var me = this;
$.each(this.data, function (i, d) {
var indent = 0;
var parent = me.parent_map[d.name];
if (parent) {
while (parent) {
indent++;
parent = me.parent_map[parent];
}
}
d.indent = indent;
});
},
export: function _export() {
var msgbox = frappe.msgprint($.format('<p>{0}</p>\
<p><input type="checkbox" name="with_groups" checked="checked"> {1}</p>\
<p><input type="checkbox" name="with_ledgers" checked="checked"> {2}</p>\
<p><button class="btn btn-primary"> {3}</button>', [__('Select To Download:'), __('With Groups'), __('With Ledgers'), __('Download')]));
var me = this;
$(msgbox.body).find("button").click(function () {
var with_groups = $(msgbox.body).find("[name='with_groups']").prop("checked");
var with_ledgers = $(msgbox.body).find("[name='with_ledgers']").prop("checked");
var data = frappe.slickgrid_tools.get_view_data(me.columns, me.dataView, function (row, item) {
if (with_groups) {
for (var i = 0; i < item.indent; i++) {
row[0] = " " + row[0];
}
}
if (with_groups && (item.is_group == 1 || item.is_group)) {
return true;
}
if (with_ledgers && item.is_group != 1 && !item.is_group) {
return true;
}
return false;
});
frappe.tools.downloadify(data, ["Report Manager", "System Manager"], me.title);
return false;
});
return false;
}
});frappe.templates['print_grid'] = ' {% if title %} <h2>{{ __(title) }}</h2> <hr> {% endif %} <table class="table table-bordered"> <thead> <tr> {% for col in columns %} {% if col.name && col._id !== "_check" %} <th style="min-width: {{ col.minWidth }}px" {% if col.docfield && in_list(["Float", "Currency", "Int"], col.docfield.fieldtype) %} class="text-right" {% endif %}>{{ __(col.name) }}</th> {% endif %} {% endfor %} </tr> </thead> <tbody> {% for row in data %} <tr> {% for col in columns %} {% if col.name && col._id !== "_check" %} {% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %} <td>{{ col.formatter ? col.formatter(row._index, col._index, value, col, row, true) : value }}</td> {% endif %} {% endfor %} </tr> {% endfor %} </tbody> </table> ';
/*!
* jquery.event.drag - v 2.2
* Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
* Open Source MIT License - http://threedubmedia.com/code/license
*/
// Created: 2008-06-04
// Updated: 2012-05-21
// REQUIRES: jquery 1.7.x
;(function( $ ){
// add the jquery instance method
$.fn.drag = function( str, arg, opts ){
// figure out the event type
var type = typeof str == "string" ? str : "",
// figure out the event handler...
fn = $.isFunction( str ) ? str : $.isFunction( arg ) ? arg : null;
// fix the event type
if ( type.indexOf("drag") !== 0 )
type = "drag"+ type;
// were options passed
opts = ( str == fn ? arg : opts ) || {};
// trigger or bind event handler
return fn ? this.bind( type, opts, fn ) : this.trigger( type );
};
// local refs (increase compression)
var $event = $.event,
$special = $event.special,
// configure the drag special event
drag = $special.drag = {
// these are the default settings
defaults: {
which: 1, // mouse button pressed to start drag sequence
distance: 0, // distance dragged before dragstart
not: ':input', // selector to suppress dragging on target elements
handle: null, // selector to match handle target elements
relative: false, // true to use "position", false to use "offset"
drop: true, // false to suppress drop events, true or selector to allow
click: false // false to suppress click events after dragend (no proxy)
},
// the key name for stored drag data
datakey: "dragdata",
// prevent bubbling for better performance
noBubble: true,
// count bound related events
add: function( obj ){
// read the interaction data
var data = $.data( this, drag.datakey ),
// read any passed options
opts = obj.data || {};
// count another realted event
data.related += 1;
// extend data options bound with this event
// don't iterate "opts" in case it is a node
$.each( drag.defaults, function( key, def ){
if ( opts[ key ] !== undefined )
data[ key ] = opts[ key ];
});
},
// forget unbound related events
remove: function(){
$.data( this, drag.datakey ).related -= 1;
},
// configure interaction, capture settings
setup: function(){
// check for related events
if ( $.data( this, drag.datakey ) )
return;
// initialize the drag data with copied defaults
var data = $.extend({ related:0 }, drag.defaults );
// store the interaction data
$.data( this, drag.datakey, data );
// bind the mousedown event, which starts drag interactions
// don't attached drag event via special for fullcalendar
// return false to attach the normal way
if(this===document) return false;
$event.add( this, "touchstart mousedown", drag.init, data );
// prevent image dragging in IE...
if ( this.attachEvent )
this.attachEvent("ondragstart", drag.dontstart );
},
// destroy configured interaction
teardown: function(){
var data = $.data( this, drag.datakey ) || {};
// check for related events
if ( data.related )
return;
// remove the stored data
$.removeData( this, drag.datakey );
// remove the mousedown event
$event.remove( this, "touchstart mousedown", drag.init );
// enable text selection
drag.textselect( true );
// un-prevent image dragging in IE...
if ( this.detachEvent )
this.detachEvent("ondragstart", drag.dontstart );
},
// initialize the interaction
init: function( event ){
// sorry, only one touch at a time
if ( drag.touched )
return;
// the drag/drop interaction data
var dd = event.data, results;
// check the which directive
if ( event.which != 0 && dd.which > 0 && event.which != dd.which )
return;
// check for suppressed selector
if ( $( event.target ).is( dd.not ) )
return;
// check for handle selector
if ( dd.handle && !$( event.target ).closest( dd.handle, event.currentTarget ).length )
return;
drag.touched = event.type == 'touchstart' ? this : null;
dd.propagates = 1;
dd.mousedown = this;
dd.interactions = [ drag.interaction( this, dd ) ];
dd.target = event.target;
dd.pageX = event.pageX;
dd.pageY = event.pageY;
dd.dragging = null;
// handle draginit event...
results = drag.hijack( event, "draginit", dd );
// early cancel
if ( !dd.propagates )
return;
// flatten the result set
results = drag.flatten( results );
// insert new interaction elements
if ( results && results.length ){
dd.interactions = [];
$.each( results, function(){
dd.interactions.push( drag.interaction( this, dd ) );
});
}
// remember how many interactions are propagating
dd.propagates = dd.interactions.length;
// locate and init the drop targets
if ( dd.drop !== false && $special.drop )
$special.drop.handler( event, dd );
// disable text selection
drag.textselect( false );
// bind additional events...
if ( drag.touched )
$event.add( drag.touched, "touchmove touchend", drag.handler, dd );
else
$event.add( document, "mousemove mouseup", drag.handler, dd );
// helps prevent text selection or scrolling
if ( !drag.touched || dd.live )
return false;
},
// returns an interaction object
interaction: function( elem, dd ){
var offset = $( elem )[ dd.relative ? "position" : "offset" ]() || { top:0, left:0 };
return {
drag: elem,
callback: new drag.callback(),
droppable: [],
offset: offset
};
},
// handle drag-releatd DOM events
handler: function( event ){
// read the data before hijacking anything
var dd = event.data;
// handle various events
switch ( event.type ){
// mousemove, check distance, start dragging
case !dd.dragging && 'touchmove':
event.preventDefault();
case !dd.dragging && 'mousemove':
// drag tolerance, x² + y² = distance²
if ( Math.pow( event.pageX-dd.pageX, 2 ) + Math.pow( event.pageY-dd.pageY, 2 ) < Math.pow( dd.distance, 2 ) )
break; // distance tolerance not reached
event.target = dd.target; // force target from "mousedown" event (fix distance issue)
drag.hijack( event, "dragstart", dd ); // trigger "dragstart"
if ( dd.propagates ) // "dragstart" not rejected
dd.dragging = true; // activate interaction
// mousemove, dragging
case 'touchmove':
event.preventDefault();
case 'mousemove':
if ( dd.dragging ){
// trigger "drag"
drag.hijack( event, "drag", dd );
if ( dd.propagates ){
// manage drop events
if ( dd.drop !== false && $special.drop )
$special.drop.handler( event, dd ); // "dropstart", "dropend"
break; // "drag" not rejected, stop
}
event.type = "mouseup"; // helps "drop" handler behave
}
// mouseup, stop dragging
case 'touchend':
case 'mouseup':
default:
if ( drag.touched )
$event.remove( drag.touched, "touchmove touchend", drag.handler ); // remove touch events
else
$event.remove( document, "mousemove mouseup", drag.handler ); // remove page events
if ( dd.dragging ){
if ( dd.drop !== false && $special.drop )
$special.drop.handler( event, dd ); // "drop"
drag.hijack( event, "dragend", dd ); // trigger "dragend"
}
drag.textselect( true ); // enable text selection
// if suppressing click events...
if ( dd.click === false && dd.dragging )
$.data( dd.mousedown, "suppress.click", new Date().getTime() + 5 );
dd.dragging = drag.touched = false; // deactivate element
break;
}
},
// re-use event object for custom events
hijack: function( event, type, dd, x, elem ){
// not configured
if ( !dd )
return;
// remember the original event and type
var orig = { event:event.originalEvent, type:event.type },
// is the event drag related or drog related?
mode = type.indexOf("drop") ? "drag" : "drop",
// iteration vars
result, i = x || 0, ia, $elems, callback,
len = !isNaN( x ) ? x : dd.interactions.length;
// modify the event type
event.type = type;
// remove the original event
event.originalEvent = null;
// initialize the results
dd.results = [];
// handle each interacted element
do if ( ia = dd.interactions[ i ] ){
// validate the interaction
if ( type !== "dragend" && ia.cancelled )
continue;
// set the dragdrop properties on the event object
callback = drag.properties( event, dd, ia );
// prepare for more results
ia.results = [];
// handle each element
$( elem || ia[ mode ] || dd.droppable ).each(function( p, subject ){
// identify drag or drop targets individually
callback.target = subject;
// force propagtion of the custom event
event.isPropagationStopped = function(){ return false; };
// handle the event
result = subject ? $event.dispatch.call( subject, event, callback ) : null;
// stop the drag interaction for this element
if ( result === false ){
if ( mode == "drag" ){
ia.cancelled = true;
dd.propagates -= 1;
}
if ( type == "drop" ){
ia[ mode ][p] = null;
}
}
// assign any dropinit elements
else if ( type == "dropinit" )
ia.droppable.push( drag.element( result ) || subject );
// accept a returned proxy element
if ( type == "dragstart" )
ia.proxy = $( drag.element( result ) || ia.drag )[0];
// remember this result
ia.results.push( result );
// forget the event result, for recycling
delete event.result;
// break on cancelled handler
if ( type !== "dropinit" )
return result;
});
// flatten the results
dd.results[ i ] = drag.flatten( ia.results );
// accept a set of valid drop targets
if ( type == "dropinit" )
ia.droppable = drag.flatten( ia.droppable );
// locate drop targets
if ( type == "dragstart" && !ia.cancelled )
callback.update();
}
while ( ++i < len )
// restore the original event & type
event.type = orig.type;
event.originalEvent = orig.event;
// return all handler results
return drag.flatten( dd.results );
},
// extend the callback object with drag/drop properties...
properties: function( event, dd, ia ){
var obj = ia.callback;
// elements
obj.drag = ia.drag;
obj.proxy = ia.proxy || ia.drag;
// starting mouse position
obj.startX = dd.pageX;
obj.startY = dd.pageY;
// current distance dragged
obj.deltaX = event.pageX - dd.pageX;
obj.deltaY = event.pageY - dd.pageY;
// original element position
obj.originalX = ia.offset.left;
obj.originalY = ia.offset.top;
// adjusted element position
obj.offsetX = obj.originalX + obj.deltaX;
obj.offsetY = obj.originalY + obj.deltaY;
// assign the drop targets information
obj.drop = drag.flatten( ( ia.drop || [] ).slice() );
obj.available = drag.flatten( ( ia.droppable || [] ).slice() );
return obj;
},
// determine is the argument is an element or jquery instance
element: function( arg ){
if ( arg && ( arg.jquery || arg.nodeType == 1 ) )
return arg;
},
// flatten nested jquery objects and arrays into a single dimension array
flatten: function( arr ){
return $.map( arr, function( member ){
return member && member.jquery ? $.makeArray( member ) :
member && member.length ? drag.flatten( member ) : member;
});
},
// toggles text selection attributes ON (true) or OFF (false)
textselect: function( bool ){
$( document )[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart )
.css("MozUserSelect", bool ? "" : "none" );
// .attr("unselectable", bool ? "off" : "on" )
document.unselectable = bool ? "off" : "on";
},
// suppress "selectstart" and "ondragstart" events
dontstart: function(){
return false;
},
// a callback instance contructor
callback: function(){}
};
// callback methods
drag.callback.prototype = {
update: function(){
if ( $special.drop && this.available.length )
$.each( this.available, function( i ){
$special.drop.locate( this, i );
});
}
};
// patch $.event.$dispatch to allow suppressing clicks
var $dispatch = $event.dispatch;
$event.dispatch = function( event ){
if ( $.data( this, "suppress."+ event.type ) - new Date().getTime() > 0 ){
$.removeData( this, "suppress."+ event.type );
return;
}
return $dispatch.apply( this, arguments );
};
// event fix hooks for touch events...
var touchHooks =
$event.fixHooks.touchstart =
$event.fixHooks.touchmove =
$event.fixHooks.touchend =
$event.fixHooks.touchcancel = {
props: "clientX clientY pageX pageY screenX screenY".split( " " ),
filter: function( event, orig ) {
if ( orig ){
var touched = ( orig.touches && orig.touches[0] )
|| ( orig.changedTouches && orig.changedTouches[0] )
|| null;
// iOS webkit: touchstart, touchmove, touchend
if ( touched )
$.each( touchHooks.props, function( i, prop ){
event[ prop ] = touched[ prop ];
});
}
return event;
}
};
// share the same special event configuration with related events...
$special.draginit = $special.dragstart = $special.dragend = drag;
})( jQuery );
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"CellRangeDecorator": CellRangeDecorator
}
});
/***
* Displays an overlay on top of a given cell range.
*
* TODO:
* Currently, it blocks mouse events to DOM nodes behind it.
* Use FF and WebKit-specific "pointer-events" CSS style, or some kind of event forwarding.
* Could also construct the borders separately using 4 individual DIVs.
*
* @param {Grid} grid
* @param {Object} options
*/
function CellRangeDecorator(grid, options) {
var _elem;
var _defaults = {
selectionCssClass: 'slick-range-decorator',
selectionCss: {
"zIndex": "9999",
"border": "2px dashed red"
}
};
options = $.extend(true, {}, _defaults, options);
function show(range) {
if (!_elem) {
_elem = $("<div></div>", {css: options.selectionCss})
.addClass(options.selectionCssClass)
.css("position", "absolute")
.appendTo(grid.getCanvasNode());
}
var from = grid.getCellNodeBox(range.fromRow, range.fromCell);
var to = grid.getCellNodeBox(range.toRow, range.toCell);
_elem.css({
top: from.top - 1,
left: from.left - 1,
height: to.bottom - from.top - 2,
width: to.right - from.left - 2
});
return _elem;
}
function hide() {
if (_elem) {
_elem.remove();
_elem = null;
}
}
$.extend(this, {
"show": show,
"hide": hide
});
}
})(jQuery);
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"CellRangeSelector": CellRangeSelector
}
});
function CellRangeSelector(options) {
var _grid;
var _canvas;
var _dragging;
var _decorator;
var _self = this;
var _handler = new Slick.EventHandler();
var _defaults = {
selectionCss: {
"border": "2px dashed blue"
}
};
function init(grid) {
options = $.extend(true, {}, _defaults, options);
_decorator = new Slick.CellRangeDecorator(grid, options);
_grid = grid;
_canvas = _grid.getCanvasNode();
_handler
.subscribe(_grid.onDragInit, handleDragInit)
.subscribe(_grid.onDragStart, handleDragStart)
.subscribe(_grid.onDrag, handleDrag)
.subscribe(_grid.onDragEnd, handleDragEnd);
}
function destroy() {
_handler.unsubscribeAll();
}
function handleDragInit(e, dd) {
// prevent the grid from cancelling drag'n'drop by default
e.stopImmediatePropagation();
}
function handleDragStart(e, dd) {
var cell = _grid.getCellFromEvent(e);
if (_self.onBeforeCellRangeSelected.notify(cell) !== false) {
if (_grid.canCellBeSelected(cell.row, cell.cell)) {
_dragging = true;
e.stopImmediatePropagation();
}
}
if (!_dragging) {
return;
}
_grid.focus();
var start = _grid.getCellFromPoint(
dd.startX - $(_canvas).offset().left,
dd.startY - $(_canvas).offset().top);
dd.range = {start: start, end: {}};
return _decorator.show(new Slick.Range(start.row, start.cell));
}
function handleDrag(e, dd) {
if (!_dragging) {
return;
}
e.stopImmediatePropagation();
var end = _grid.getCellFromPoint(
e.pageX - $(_canvas).offset().left,
e.pageY - $(_canvas).offset().top);
if (!_grid.canCellBeSelected(end.row, end.cell)) {
return;
}
dd.range.end = end;
_decorator.show(new Slick.Range(dd.range.start.row, dd.range.start.cell, end.row, end.cell));
}
function handleDragEnd(e, dd) {
if (!_dragging) {
return;
}
_dragging = false;
e.stopImmediatePropagation();
_decorator.hide();
_self.onCellRangeSelected.notify({
range: new Slick.Range(
dd.range.start.row,
dd.range.start.cell,
dd.range.end.row,
dd.range.end.cell
)
});
}
$.extend(this, {
"init": init,
"destroy": destroy,
"onBeforeCellRangeSelected": new Slick.Event(),
"onCellRangeSelected": new Slick.Event()
});
}
})(jQuery);(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"CellSelectionModel": CellSelectionModel
}
});
function CellSelectionModel(options) {
var _grid;
var _canvas;
var _ranges = [];
var _self = this;
var _selector = new Slick.CellRangeSelector({
"selectionCss": {
"border": "2px solid black"
}
});
var _options;
var _defaults = {
selectActiveCell: true
};
function init(grid) {
_options = $.extend(true, {}, _defaults, options);
_grid = grid;
_canvas = _grid.getCanvasNode();
_grid.onActiveCellChanged.subscribe(handleActiveCellChange);
_grid.onKeyDown.subscribe(handleKeyDown);
grid.registerPlugin(_selector);
_selector.onCellRangeSelected.subscribe(handleCellRangeSelected);
_selector.onBeforeCellRangeSelected.subscribe(handleBeforeCellRangeSelected);
}
function destroy() {
_grid.onActiveCellChanged.unsubscribe(handleActiveCellChange);
_grid.onKeyDown.unsubscribe(handleKeyDown);
_selector.onCellRangeSelected.unsubscribe(handleCellRangeSelected);
_selector.onBeforeCellRangeSelected.unsubscribe(handleBeforeCellRangeSelected);
_grid.unregisterPlugin(_selector);
}
function removeInvalidRanges(ranges) {
var result = [];
for (var i = 0; i < ranges.length; i++) {
var r = ranges[i];
if (_grid.canCellBeSelected(r.fromRow, r.fromCell) && _grid.canCellBeSelected(r.toRow, r.toCell)) {
result.push(r);
}
}
return result;
}
function setSelectedRanges(ranges) {
_ranges = removeInvalidRanges(ranges);
_self.onSelectedRangesChanged.notify(_ranges);
}
function getSelectedRanges() {
return _ranges;
}
function handleBeforeCellRangeSelected(e, args) {
if (_grid.getEditorLock().isActive()) {
e.stopPropagation();
return false;
}
}
function handleCellRangeSelected(e, args) {
setSelectedRanges([args.range]);
}
function handleActiveCellChange(e, args) {
if (_options.selectActiveCell && args.row != null && args.cell != null) {
setSelectedRanges([new Slick.Range(args.row, args.cell)]);
}
}
function handleKeyDown(e) {
/***
* Кey codes
* 37 left
* 38 up
* 39 right
* 40 down
*/
var ranges, last;
var active = _grid.getActiveCell();
if ( active && e.shiftKey && !e.ctrlKey && !e.altKey &&
(e.which == 37 || e.which == 39 || e.which == 38 || e.which == 40) ) {
ranges = getSelectedRanges();
if (!ranges.length)
ranges.push(new Slick.Range(active.row, active.cell));
// keyboard can work with last range only
last = ranges.pop();
// can't handle selection out of active cell
if (!last.contains(active.row, active.cell))
last = new Slick.Range(active.row, active.cell);
var dRow = last.toRow - last.fromRow,
dCell = last.toCell - last.fromCell,
// walking direction
dirRow = active.row == last.fromRow ? 1 : -1,
dirCell = active.cell == last.fromCell ? 1 : -1;
if (e.which == 37) {
dCell -= dirCell;
} else if (e.which == 39) {
dCell += dirCell ;
} else if (e.which == 38) {
dRow -= dirRow;
} else if (e.which == 40) {
dRow += dirRow;
}
// define new selection range
var new_last = new Slick.Range(active.row, active.cell, active.row + dirRow*dRow, active.cell + dirCell*dCell);
if (removeInvalidRanges([new_last]).length) {
ranges.push(new_last);
var viewRow = dirRow > 0 ? new_last.toRow : new_last.fromRow;
var viewCell = dirCell > 0 ? new_last.toCell : new_last.fromCell;
_grid.scrollRowIntoView(viewRow);
_grid.scrollCellIntoView(viewRow, viewCell);
}
else
ranges.push(last);
setSelectedRanges(ranges);
e.preventDefault();
e.stopPropagation();
}
}
$.extend(this, {
"getSelectedRanges": getSelectedRanges,
"setSelectedRanges": setSelectedRanges,
"init": init,
"destroy": destroy,
"onSelectedRangesChanged": new Slick.Event()
});
}
})(jQuery);
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"CellExternalCopyManager": CellExternalCopyManager
}
});
function CellExternalCopyManager(options) {
/*
This manager enables users to copy/paste data from/to an external Spreadsheet application
Since it is not possible to access directly the clipboard in javascript, the plugin uses
a trick to do it's job. After detecting the keystroke, we dynamically create a textarea
where the browser copies/pastes the serialized data.
options:
copiedCellStyle : sets the css className used for copied cells. default : "copied"
copiedCellStyleLayerKey : sets the layer key for setting css values of copied cells. default : "copy-manager"
dataItemColumnValueExtractor : option to specify a custom column value extractor function
dataItemColumnValueSetter : option to specify a custom column value setter function
*/
var _grid;
var _self = this;
var _copiedRanges;
var _options = options || {};
var _copiedCellStyleLayerKey = _options.copiedCellStyleLayerKey || "copy-manager";
var _copiedCellStyle = _options.copiedCellStyle || "copied";
var _clearCopyTI = 0;
var keyCodes = {
'C':67,
'V':86
}
function init(grid) {
_grid = grid;
_grid.onKeyDown.subscribe(handleKeyDown);
// we need a cell selection model
var cellSelectionModel = grid.getSelectionModel();
if (!cellSelectionModel){
throw new Error("Selection model is mandatory for this plugin. Please set a selection model on the grid before adding this plugin: grid.setSelectionModel(new Slick.CellSelectionModel())");
}
// we give focus on the grid when a selection is done on it.
// without this, if the user selects a range of cell without giving focus on a particular cell, the grid doesn't get the focus and key stroke handles (ctrl+c) don't work
cellSelectionModel.onSelectedRangesChanged.subscribe(function(e, args){
_grid.focus();
});
}
function destroy() {
_grid.onKeyDown.unsubscribe(handleKeyDown);
}
function getDataItemValueForColumn(item, columnDef) {
if (_options.dataItemColumnValueExtractor) {
return _options.dataItemColumnValueExtractor(item, columnDef);
}
// if a custom getter is not defined, we call serializeValue of the editor to serialize
var editorArgs = {
'container':$(document), // a dummy container
'column':columnDef
};
var editor = new columnDef.editor(editorArgs);
var retVal = '';
editor.loadValue(item);
retVal = editor.serializeValue();
editor.destroy();
return retVal;
}
function setDataItemValueForColumn(item, columnDef, value) {
if (_options.dataItemColumnValueSetter) {
return _options.dataItemColumnValueSetter(item, columnDef, value);
}
// if a custom setter is not defined, we call applyValue of the editor to unserialize
var editorArgs = {
'container':$(document), // a dummy container
'column':columnDef
};
var editor = new columnDef.editor(editorArgs);
editor.loadValue(item);
editor.applyValue(item, value);
editor.destroy();
}
function _createTextBox(innerText){
var ta = document.createElement('textarea');
ta.style.position = 'absolute';
ta.style.left = '-1000px';
ta.style.top = '-1000px';
ta.value = innerText;
document.body.appendChild(ta);
ta.focus();
return ta;
}
function _decodeTabularData(_grid, ta){
var columns = _grid.getColumns();
var clipText = ta.value;
var clipRows = clipText.split(/[\n\f\r]/);
var clippedRange = [];
document.body.removeChild(ta);
for (var i=0; i<clipRows.length; i++) {
if (clipRows[i]!="")
clippedRange[i] = clipRows[i].split("\t");
}
var selectedCell = _grid.getActiveCell();
var ranges = _grid.getSelectionModel().getSelectedRanges();
var selectedRange = ranges && ranges.length ? ranges[0] : null; // pick only one selection
var activeRow = null;
var activeCell = null;
if (selectedRange){
activeRow = selectedRange.fromRow;
activeCell = selectedRange.fromCell;
} else if (selectedCell){
activeRow = selectedCell.row;
activeCell = selectedCell.cell;
} else {
// we don't know where to paste
return;
}
var oneCellToMultiple = false;
var destH = clippedRange.length;
var destW = clippedRange.length ? clippedRange[0].length : 0;
if (clippedRange.length == 1 && clippedRange[0].length == 1 && selectedRange){
oneCellToMultiple = true;
destH = selectedRange.toRow - selectedRange.fromRow +1;
destW = selectedRange.toCell - selectedRange.fromCell +1;
}
var desty = activeRow;
var destx = activeCell;
var h = 0;
var w = 0;
for (var y = 0; y < destH; y++){
h++;
w=0;
for (var x = 0; x < destW; x++){
w++;
var desty = activeRow + y;
var destx = activeCell + x;
if (desty < data.length && destx < grid.getColumns().length ) {
var nd = _grid.getCellNode(desty, destx);
var dt = _grid.getDataItem(desty);
if (oneCellToMultiple)
setDataItemValueForColumn(dt, columns[destx], clippedRange[0][0]);
else
setDataItemValueForColumn(dt, columns[destx], clippedRange[y][x]);
_grid.updateCell(desty, destx);
}
}
}
var bRange = {
'fromCell': activeCell,
'fromRow': activeRow,
'toCell': activeCell+w-1,
'toRow': activeRow+h-1
}
markCopySelection([bRange]);
_grid.getSelectionModel().setSelectedRanges([bRange]);
_self.onPasteCells.notify({ranges: [bRange]});
}
function handleKeyDown(e, args) {
var ranges;
if (!_grid.getEditorLock().isActive()) {
if (e.which == frappe.ui.keyCode.ESCAPE) {
if (_copiedRanges) {
e.preventDefault();
clearCopySelection();
_self.onCopyCancelled.notify({ranges: _copiedRanges});
_copiedRanges = null;
}
}
if (e.which == keyCodes.C && (e.ctrlKey || e.metaKey)) { // CTRL + C
ranges = _grid.getSelectionModel().getSelectedRanges();
if (ranges.length != 0) {
_copiedRanges = ranges;
markCopySelection(ranges);
_self.onCopyCells.notify({ranges: ranges});
var columns = _grid.getColumns();
var clipTextArr = [];
for (var rg = 0; rg < ranges.length; rg++){
var range = ranges[rg];
var clipTextRows = [];
for (var i=range.fromRow; i< range.toRow+1 ; i++){
var clipTextCells = [];
var dt = _grid.getDataItem(i);
for (var j=range.fromCell; j< range.toCell+1 ; j++){
clipTextCells.push(getDataItemValueForColumn(dt, columns[j]));
}
clipTextRows.push(clipTextCells.join("\t"));
}
clipTextArr.push(clipTextRows.join("\r\n"));
}
var clipText = clipTextArr.join('');
var ta = _createTextBox(clipText);
$(ta).select();
setTimeout(function(){
document.body.removeChild(ta);
}, 100);
return false;
}
}
if (e.which == keyCodes.V && (e.ctrlKey || e.metaKey)) { // CTRL + V
var ta = _createTextBox('');
setTimeout(function(){
_decodeTabularData(_grid, ta);
}, 100);
return false;
}
}
}
function markCopySelection(ranges) {
clearCopySelection();
var columns = _grid.getColumns();
var hash = {};
for (var i = 0; i < ranges.length; i++) {
for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
hash[j] = {};
for (var k = ranges[i].fromCell; k <= ranges[i].toCell && k<columns.length; k++) {
hash[j][columns[k].id] = _copiedCellStyle;
}
}
}
_grid.setCellCssStyles(_copiedCellStyleLayerKey, hash);
clearTimeout(_clearCopyTI);
_clearCopyTI = setTimeout(function(){
_self.clearCopySelection();
}, 2000);
}
function clearCopySelection() {
_grid.removeCellCssStyles(_copiedCellStyleLayerKey);
}
$.extend(this, {
"init": init,
"destroy": destroy,
"clearCopySelection": clearCopySelection,
"handleKeyDown":handleKeyDown,
"onCopyCells": new Slick.Event(),
"onCopyCancelled": new Slick.Event(),
"onPasteCells": new Slick.Event()
});
}
})(jQuery);/***
* Contains core SlickGrid classes.
* @module Core
* @namespace Slick
*/
(function ($) {
// register namespace
$.extend(true, window, {
"Slick": {
"Event": Event,
"EventData": EventData,
"EventHandler": EventHandler,
"Range": Range,
"NonDataRow": NonDataItem,
"Group": Group,
"GroupTotals": GroupTotals,
"EditorLock": EditorLock,
/***
* A global singleton editor lock.
* @class GlobalEditorLock
* @static
* @constructor
*/
"GlobalEditorLock": new EditorLock()
}
});
/***
* An event object for passing data to event handlers and letting them control propagation.
* <p>This is pretty much identical to how W3C and jQuery implement events.</p>
* @class EventData
* @constructor
*/
function EventData() {
var isPropagationStopped = false;
var isImmediatePropagationStopped = false;
/***
* Stops event from propagating up the DOM tree.
* @method stopPropagation
*/
this.stopPropagation = function () {
isPropagationStopped = true;
};
/***
* Returns whether stopPropagation was called on this event object.
* @method isPropagationStopped
* @return {Boolean}
*/
this.isPropagationStopped = function () {
return isPropagationStopped;
};
/***
* Prevents the rest of the handlers from being executed.
* @method stopImmediatePropagation
*/
this.stopImmediatePropagation = function () {
isImmediatePropagationStopped = true;
};
/***
* Returns whether stopImmediatePropagation was called on this event object.\
* @method isImmediatePropagationStopped
* @return {Boolean}
*/
this.isImmediatePropagationStopped = function () {
return isImmediatePropagationStopped;
}
}
/***
* A simple publisher-subscriber implementation.
* @class Event
* @constructor
*/
function Event() {
var handlers = [];
/***
* Adds an event handler to be called when the event is fired.
* <p>Event handler will receive two arguments - an <code>EventData</code> and the <code>data</code>
* object the event was fired with.<p>
* @method subscribe
* @param fn {Function} Event handler.
*/
this.subscribe = function (fn) {
handlers.push(fn);
};
/***
* Removes an event handler added with <code>subscribe(fn)</code>.
* @method unsubscribe
* @param fn {Function} Event handler to be removed.
*/
this.unsubscribe = function (fn) {
for (var i = handlers.length - 1; i >= 0; i--) {
if (handlers[i] === fn) {
handlers.splice(i, 1);
}
}
};
/***
* Fires an event notifying all subscribers.
* @method notify
* @param args {Object} Additional data object to be passed to all handlers.
* @param e {EventData}
* Optional.
* An <code>EventData</code> object to be passed to all handlers.
* For DOM events, an existing W3C/jQuery event object can be passed in.
* @param scope {Object}
* Optional.
* The scope ("this") within which the handler will be executed.
* If not specified, the scope will be set to the <code>Event</code> instance.
*/
this.notify = function (args, e, scope) {
e = e || new EventData();
scope = scope || this;
var returnValue;
for (var i = 0; i < handlers.length && !(e.isPropagationStopped() || e.isImmediatePropagationStopped()); i++) {
returnValue = handlers[i].call(scope, e, args);
}
return returnValue;
};
}
function EventHandler() {
var handlers = [];
this.subscribe = function (event, handler) {
handlers.push({
event: event,
handler: handler
});
event.subscribe(handler);
return this; // allow chaining
};
this.unsubscribe = function (event, handler) {
var i = handlers.length;
while (i--) {
if (handlers[i].event === event &&
handlers[i].handler === handler) {
handlers.splice(i, 1);
event.unsubscribe(handler);
return;
}
}
return this; // allow chaining
};
this.unsubscribeAll = function () {
var i = handlers.length;
while (i--) {
handlers[i].event.unsubscribe(handlers[i].handler);
}
handlers = [];
return this; // allow chaining
}
}
/***
* A structure containing a range of cells.
* @class Range
* @constructor
* @param fromRow {Integer} Starting row.
* @param fromCell {Integer} Starting cell.
* @param toRow {Integer} Optional. Ending row. Defaults to <code>fromRow</code>.
* @param toCell {Integer} Optional. Ending cell. Defaults to <code>fromCell</code>.
*/
function Range(fromRow, fromCell, toRow, toCell) {
if (toRow === undefined && toCell === undefined) {
toRow = fromRow;
toCell = fromCell;
}
/***
* @property fromRow
* @type {Integer}
*/
this.fromRow = Math.min(fromRow, toRow);
/***
* @property fromCell
* @type {Integer}
*/
this.fromCell = Math.min(fromCell, toCell);
/***
* @property toRow
* @type {Integer}
*/
this.toRow = Math.max(fromRow, toRow);
/***
* @property toCell
* @type {Integer}
*/
this.toCell = Math.max(fromCell, toCell);
/***
* Returns whether a range represents a single row.
* @method isSingleRow
* @return {Boolean}
*/
this.isSingleRow = function () {
return this.fromRow == this.toRow;
};
/***
* Returns whether a range represents a single cell.
* @method isSingleCell
* @return {Boolean}
*/
this.isSingleCell = function () {
return this.fromRow == this.toRow && this.fromCell == this.toCell;
};
/***
* Returns whether a range contains a given cell.
* @method contains
* @param row {Integer}
* @param cell {Integer}
* @return {Boolean}
*/
this.contains = function (row, cell) {
return row >= this.fromRow && row <= this.toRow &&
cell >= this.fromCell && cell <= this.toCell;
};
/***
* Returns a readable representation of a range.
* @method toString
* @return {String}
*/
this.toString = function () {
if (this.isSingleCell()) {
return "(" + this.fromRow + ":" + this.fromCell + ")";
}
else {
return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")";
}
}
}
/***
* A base class that all special / non-data rows (like Group and GroupTotals) derive from.
* @class NonDataItem
* @constructor
*/
function NonDataItem() {
this.__nonDataRow = true;
}
/***
* Information about a group of rows.
* @class Group
* @extends Slick.NonDataItem
* @constructor
*/
function Group() {
this.__group = true;
/**
* Grouping level, starting with 0.
* @property level
* @type {Number}
*/
this.level = 0;
/***
* Number of rows in the group.
* @property count
* @type {Integer}
*/
this.count = 0;
/***
* Grouping value.
* @property value
* @type {Object}
*/
this.value = null;
/***
* Formatted display value of the group.
* @property title
* @type {String}
*/
this.title = null;
/***
* Whether a group is collapsed.
* @property collapsed
* @type {Boolean}
*/
this.collapsed = false;
/***
* GroupTotals, if any.
* @property totals
* @type {GroupTotals}
*/
this.totals = null;
/**
* Rows that are part of the group.
* @property rows
* @type {Array}
*/
this.rows = [];
/**
* Sub-groups that are part of the group.
* @property groups
* @type {Array}
*/
this.groups = null;
/**
* A unique key used to identify the group. This key can be used in calls to DataView
* collapseGroup() or expandGroup().
* @property groupingKey
* @type {Object}
*/
this.groupingKey = null;
}
Group.prototype = new NonDataItem();
/***
* Compares two Group instances.
* @method equals
* @return {Boolean}
* @param group {Group} Group instance to compare to.
*/
Group.prototype.equals = function (group) {
return this.value === group.value &&
this.count === group.count &&
this.collapsed === group.collapsed &&
this.title === group.title;
};
/***
* Information about group totals.
* An instance of GroupTotals will be created for each totals row and passed to the aggregators
* so that they can store arbitrary data in it. That data can later be accessed by group totals
* formatters during the display.
* @class GroupTotals
* @extends Slick.NonDataItem
* @constructor
*/
function GroupTotals() {
this.__groupTotals = true;
/***
* Parent Group.
* @param group
* @type {Group}
*/
this.group = null;
/***
* Whether the totals have been fully initialized / calculated.
* Will be set to false for lazy-calculated group totals.
* @param initialized
* @type {Boolean}
*/
this.initialized = false;
}
GroupTotals.prototype = new NonDataItem();
/***
* A locking helper to track the active edit controller and ensure that only a single controller
* can be active at a time. This prevents a whole class of state and validation synchronization
* issues. An edit controller (such as SlickGrid) can query if an active edit is in progress
* and attempt a commit or cancel before proceeding.
* @class EditorLock
* @constructor
*/
function EditorLock() {
var activeEditController = null;
/***
* Returns true if a specified edit controller is active (has the edit lock).
* If the parameter is not specified, returns true if any edit controller is active.
* @method isActive
* @param editController {EditController}
* @return {Boolean}
*/
this.isActive = function (editController) {
return (editController ? activeEditController === editController : activeEditController !== null);
};
/***
* Sets the specified edit controller as the active edit controller (acquire edit lock).
* If another edit controller is already active, and exception will be thrown.
* @method activate
* @param editController {EditController} edit controller acquiring the lock
*/
this.activate = function (editController) {
if (editController === activeEditController) { // already activated?
return;
}
if (activeEditController !== null) {
throw "SlickGrid.EditorLock.activate: an editController is still active, can't activate another editController";
}
if (!editController.commitCurrentEdit) {
throw "SlickGrid.EditorLock.activate: editController must implement .commitCurrentEdit()";
}
if (!editController.cancelCurrentEdit) {
throw "SlickGrid.EditorLock.activate: editController must implement .cancelCurrentEdit()";
}
activeEditController = editController;
};
/***
* Unsets the specified edit controller as the active edit controller (release edit lock).
* If the specified edit controller is not the active one, an exception will be thrown.
* @method deactivate
* @param editController {EditController} edit controller releasing the lock
*/
this.deactivate = function (editController) {
if (activeEditController !== editController) {
throw "SlickGrid.EditorLock.deactivate: specified editController is not the currently active one";
}
activeEditController = null;
};
/***
* Attempts to commit the current edit by calling "commitCurrentEdit" method on the active edit
* controller and returns whether the commit attempt was successful (commit may fail due to validation
* errors, etc.). Edit controller's "commitCurrentEdit" must return true if the commit has succeeded
* and false otherwise. If no edit controller is active, returns true.
* @method commitCurrentEdit
* @return {Boolean}
*/
this.commitCurrentEdit = function () {
return (activeEditController ? activeEditController.commitCurrentEdit() : true);
};
/***
* Attempts to cancel the current edit by calling "cancelCurrentEdit" method on the active edit
* controller and returns whether the edit was successfully cancelled. If no edit controller is
* active, returns true.
* @method cancelCurrentEdit
* @return {Boolean}
*/
this.cancelCurrentEdit = function cancelCurrentEdit() {
return (activeEditController ? activeEditController.cancelCurrentEdit() : true);
};
}
})(jQuery);
/**
* @license
* (c) 2009-2013 Michael Leibman
* michael{dot}leibman{at}gmail{dot}com
* http://github.com/mleibman/slickgrid
*
* Distributed under MIT license.
* All rights reserved.
*
* SlickGrid v2.2
*
* NOTES:
* Cell/row DOM manipulations are done directly bypassing jQuery's DOM manipulation methods.
* This increases the speed dramatically, but can only be done safely because there are no event handlers
* or data associated with any cell/row DOM nodes. Cell editors must make sure they implement .destroy()
* and do proper cleanup.
*/
// make sure required JavaScript modules are loaded
if (typeof jQuery === "undefined") {
throw "SlickGrid requires jquery module to be loaded";
}
if (!jQuery.fn.drag) {
throw "SlickGrid requires jquery.event.drag module to be loaded";
}
if (typeof Slick === "undefined") {
throw "slick.core.js not loaded";
}
(function ($) {
// Slick.Grid
$.extend(true, window, {
Slick: {
Grid: SlickGrid
}
});
// shared across all grids on the page
var scrollbarDimensions;
var maxSupportedCssHeight; // browser's breaking point
//////////////////////////////////////////////////////////////////////////////////////////////
// SlickGrid class implementation (available as Slick.Grid)
/**
* Creates a new instance of the grid.
* @class SlickGrid
* @constructor
* @param {Node} container Container node to create the grid in.
* @param {Array,Object} data An array of objects for databinding.
* @param {Array} columns An array of column definitions.
* @param {Object} options Grid options.
**/
function SlickGrid(container, data, columns, options) {
// settings
var defaults = {
explicitInitialization: false,
rowHeight: 25,
defaultColumnWidth: 80,
enableAddRow: false,
leaveSpaceForNewRows: false,
editable: false,
autoEdit: true,
enableCellNavigation: true,
enableColumnReorder: true,
asyncEditorLoading: false,
asyncEditorLoadDelay: 100,
forceFitColumns: false,
enableAsyncPostRender: false,
asyncPostRenderDelay: 50,
autoHeight: false,
editorLock: Slick.GlobalEditorLock,
showHeaderRow: false,
headerRowHeight: 25,
showTopPanel: false,
topPanelHeight: 25,
formatterFactory: null,
editorFactory: null,
cellFlashingCssClass: "flashing",
selectedCellCssClass: "selected",
multiSelect: true,
enableTextSelectionOnCells: false,
dataItemColumnValueExtractor: null,
fullWidthRows: false,
multiColumnSort: false,
defaultFormatter: defaultFormatter,
forceSyncScrolling: false,
addNewRowCssClass: "new-row"
};
var columnDefaults = {
name: "",
resizable: true,
sortable: false,
minWidth: 30,
rerenderOnResize: false,
headerCssClass: null,
defaultSortAsc: true,
focusable: true,
selectable: true
};
// scroller
var th; // virtual height
var h; // real scrollable height
var ph; // page height
var n; // number of pages
var cj; // "jumpiness" coefficient
var page = 0; // current page
var offset = 0; // current page offset
var vScrollDir = 1;
// private
var initialized = false;
var $container;
var uid = "slickgrid_" + Math.round(1000000 * Math.random());
var self = this;
var $focusSink, $focusSink2;
var $headerScroller;
var $headers;
var $headerRow, $headerRowScroller, $headerRowSpacer;
var $topPanelScroller;
var $topPanel;
var $viewport;
var $canvas;
var $style;
var $boundAncestors;
var stylesheet, columnCssRulesL, columnCssRulesR;
var viewportH, viewportW;
var canvasWidth;
var viewportHasHScroll, viewportHasVScroll;
var headerColumnWidthDiff = 0, headerColumnHeightDiff = 0, // border+padding
cellWidthDiff = 0, cellHeightDiff = 0;
var absoluteColumnMinWidth;
var tabbingDirection = 1;
var activePosX;
var activeRow, activeCell;
var activeCellNode = null;
var currentEditor = null;
var serializedEditorValue;
var editController;
var rowsCache = {};
var renderedRows = 0;
var numVisibleRows;
var prevScrollTop = 0;
var scrollTop = 0;
var lastRenderedScrollTop = 0;
var lastRenderedScrollLeft = 0;
var prevScrollLeft = 0;
var scrollLeft = 0;
var selectionModel;
var selectedRows = [];
var plugins = [];
var cellCssClasses = {};
var columnsById = {};
var sortColumns = [];
var columnPosLeft = [];
var columnPosRight = [];
// async call handles
var h_editorLoader = null;
var h_render = null;
var h_postrender = null;
var postProcessedRows = {};
var postProcessToRow = null;
var postProcessFromRow = null;
// perf counters
var counter_rows_rendered = 0;
var counter_rows_removed = 0;
// These two variables work around a bug with inertial scrolling in Webkit/Blink on Mac.
// See http://crbug.com/312427.
var rowNodeFromLastMouseWheelEvent; // this node must not be deleted while inertial scrolling
var zombieRowNodeFromLastMouseWheelEvent; // node that was hidden instead of getting deleted
//////////////////////////////////////////////////////////////////////////////////////////////
// Initialization
function init() {
$container = $(container);
if ($container.length < 1) {
throw new Error("SlickGrid requires a valid container, " + container + " does not exist in the DOM.");
}
// calculate these only once and share between grid instances
maxSupportedCssHeight = maxSupportedCssHeight || getMaxSupportedCssHeight();
scrollbarDimensions = scrollbarDimensions || measureScrollbar();
options = $.extend({}, defaults, options);
validateAndEnforceOptions();
columnDefaults.width = options.defaultColumnWidth;
columnsById = {};
for (var i = 0; i < columns.length; i++) {
var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
columnsById[m.id] = i;
if (m.minWidth && m.width < m.minWidth) {
m.width = m.minWidth;
}
if (m.maxWidth && m.width > m.maxWidth) {
m.width = m.maxWidth;
}
}
// validate loaded JavaScript modules against requested options
if (options.enableColumnReorder && !$.fn.sortable) {
throw new Error("SlickGrid's 'enableColumnReorder = true' option requires jquery-ui.sortable module to be loaded");
}
editController = {
"commitCurrentEdit": commitCurrentEdit,
"cancelCurrentEdit": cancelCurrentEdit
};
$container
.empty()
.css("overflow", "hidden")
.css("outline", 0)
.addClass(uid)
.addClass("ui-widget");
// set up a positioning container if needed
if (!/relative|absolute|fixed/.test($container.css("position"))) {
$container.css("position", "relative");
}
$focusSink = $("<div tabIndex='0' hideFocus style='position:fixed;width:0;height:0;top:0;left:0;outline:0;'></div>").appendTo($container);
$headerScroller = $("<div class='slick-header ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
$headers = $("<div class='slick-header-columns' style='left:-1000px' />").appendTo($headerScroller);
$headers.width(getHeadersWidth());
$headerRowScroller = $("<div class='slick-headerrow ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
$headerRow = $("<div class='slick-headerrow-columns' />").appendTo($headerRowScroller);
$headerRowSpacer = $("<div style='display:block;height:1px;position:absolute;top:0;left:0;'></div>")
.css("width", getCanvasWidth() + scrollbarDimensions.width + "px")
.appendTo($headerRowScroller);
$topPanelScroller = $("<div class='slick-top-panel-scroller ui-state-default' style='overflow:hidden;position:relative;' />").appendTo($container);
$topPanel = $("<div class='slick-top-panel' style='width:10000px' />").appendTo($topPanelScroller);
if (!options.showTopPanel) {
$topPanelScroller.hide();
}
if (!options.showHeaderRow) {
$headerRowScroller.hide();
}
$viewport = $("<div class='slick-viewport' style='width:100%;overflow:auto;outline:0;position:relative;;'>").appendTo($container);
$viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
$canvas = $("<div class='grid-canvas' />").appendTo($viewport);
$focusSink2 = $focusSink.clone().appendTo($container);
if (!options.explicitInitialization) {
finishInitialization();
}
}
function finishInitialization() {
if (!initialized) {
initialized = true;
viewportW = parseFloat($.css($container[0], "width", true));
// header columns and cells may have different padding/border skewing width calculations (box-sizing, hello?)
// calculate the diff so we can set consistent sizes
measureCellPaddingAndBorder();
// for usability reasons, all text selection in SlickGrid is disabled
// with the exception of input and textarea elements (selection must
// be enabled there so that editors work as expected); note that
// selection in grid cells (grid body) is already unavailable in
// all browsers except IE
disableSelection($headers); // disable all text selection in header (including input and textarea)
if (!options.enableTextSelectionOnCells) {
// disable text selection in grid cells except in input and textarea elements
// (this is IE-specific, because selectstart event will only fire in IE)
$viewport.bind("selectstart.ui", function (event) {
return $(event.target).is("input,textarea");
});
}
updateColumnCaches();
createColumnHeaders();
setupColumnSort();
createCssRules();
resizeCanvas();
bindAncestorScrollEvents();
$container
.bind("resize.slickgrid", resizeCanvas);
$viewport
//.bind("click", handleClick)
.bind("scroll", handleScroll);
$headerScroller
.bind("contextmenu", handleHeaderContextMenu)
.bind("click", handleHeaderClick)
.delegate(".slick-header-column", "mouseenter", handleHeaderMouseEnter)
.delegate(".slick-header-column", "mouseleave", handleHeaderMouseLeave);
$headerRowScroller
.bind("scroll", handleHeaderRowScroll);
$focusSink.add($focusSink2)
.bind("keydown", handleKeyDown);
$canvas
.bind("keydown", handleKeyDown)
.bind("click", handleClick)
.bind("dblclick", handleDblClick)
.bind("contextmenu", handleContextMenu)
.bind("draginit", handleDragInit)
.bind("dragstart", {distance: 3}, handleDragStart)
.bind("drag", handleDrag)
.bind("dragend", handleDragEnd)
.delegate(".slick-cell", "mouseenter", handleMouseEnter)
.delegate(".slick-cell", "mouseleave", handleMouseLeave);
// Work around http://crbug.com/312427.
if (navigator.userAgent.toLowerCase().match(/webkit/) &&
navigator.userAgent.toLowerCase().match(/macintosh/)) {
$canvas.bind("mousewheel", handleMouseWheel);
}
}
}
function registerPlugin(plugin) {
plugins.unshift(plugin);
plugin.init(self);
}
function unregisterPlugin(plugin) {
for (var i = plugins.length; i >= 0; i--) {
if (plugins[i] === plugin) {
if (plugins[i].destroy) {
plugins[i].destroy();
}
plugins.splice(i, 1);
break;
}
}
}
function setSelectionModel(model) {
if (selectionModel) {
selectionModel.onSelectedRangesChanged.unsubscribe(handleSelectedRangesChanged);
if (selectionModel.destroy) {
selectionModel.destroy();
}
}
selectionModel = model;
if (selectionModel) {
selectionModel.init(self);
selectionModel.onSelectedRangesChanged.subscribe(handleSelectedRangesChanged);
}
}
function getSelectionModel() {
return selectionModel;
}
function getCanvasNode() {
return $canvas[0];
}
function measureScrollbar() {
var $c = $("<div style='position:absolute; top:-10000px; left:-10000px; width:100px; height:100px; overflow:scroll;'></div>").appendTo("body");
var dim = {
width: $c.width() - $c[0].clientWidth,
height: $c.height() - $c[0].clientHeight
};
$c.remove();
return dim;
}
function getHeadersWidth() {
var headersWidth = 0;
for (var i = 0, ii = columns.length; i < ii; i++) {
var width = columns[i].width;
headersWidth += width;
}
headersWidth += scrollbarDimensions.width;
return Math.max(headersWidth, viewportW) + 1000;
}
function getCanvasWidth() {
var availableWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
var rowWidth = 0;
var i = columns.length;
while (i--) {
rowWidth += columns[i].width;
}
return options.fullWidthRows ? Math.max(rowWidth, availableWidth) : rowWidth;
}
function updateCanvasWidth(forceColumnWidthsUpdate) {
var oldCanvasWidth = canvasWidth;
canvasWidth = getCanvasWidth();
if (canvasWidth != oldCanvasWidth) {
$canvas.width(canvasWidth);
$headerRow.width(canvasWidth);
$headers.width(getHeadersWidth());
viewportHasHScroll = (canvasWidth > viewportW - scrollbarDimensions.width);
}
$headerRowSpacer.width(canvasWidth + (viewportHasVScroll ? scrollbarDimensions.width : 0));
if (canvasWidth != oldCanvasWidth || forceColumnWidthsUpdate) {
applyColumnWidths();
}
}
function disableSelection($target) {
if ($target && $target.jquery) {
$target
.attr("unselectable", "on")
.css("MozUserSelect", "none")
.bind("selectstart.ui", function () {
return false;
}); // from jquery:ui.core.js 1.7.2
}
}
function getMaxSupportedCssHeight() {
var supportedHeight = 1000000;
// FF reports the height back but still renders blank after ~6M px
var testUpTo = navigator.userAgent.toLowerCase().match(/firefox/) ? 6000000 : 1000000000;
var div = $("<div style='display:none' />").appendTo(document.body);
while (true) {
var test = supportedHeight * 2;
div.css("height", test);
if (test > testUpTo || div.height() !== test) {
break;
} else {
supportedHeight = test;
}
}
div.remove();
return supportedHeight;
}
// TODO: this is static. need to handle page mutation.
function bindAncestorScrollEvents() {
var elem = $canvas[0];
while ((elem = elem.parentNode) != document.body && elem != null) {
// bind to scroll containers only
if (elem == $viewport[0] || elem.scrollWidth != elem.clientWidth || elem.scrollHeight != elem.clientHeight) {
var $elem = $(elem);
if (!$boundAncestors) {
$boundAncestors = $elem;
} else {
$boundAncestors = $boundAncestors.add($elem);
}
$elem.bind("scroll." + uid, handleActiveCellPositionChange);
}
}
}
function unbindAncestorScrollEvents() {
if (!$boundAncestors) {
return;
}
$boundAncestors.unbind("scroll." + uid);
$boundAncestors = null;
}
function updateColumnHeader(columnId, title, toolTip) {
if (!initialized) { return; }
var idx = getColumnIndex(columnId);
if (idx == null) {
return;
}
var columnDef = columns[idx];
var $header = $headers.children().eq(idx);
if ($header) {
if (title !== undefined) {
columns[idx].name = title;
}
if (toolTip !== undefined) {
columns[idx].toolTip = toolTip;
}
trigger(self.onBeforeHeaderCellDestroy, {
"node": $header[0],
"column": columnDef
});
$header
.attr("title", toolTip || "")
.children().eq(0).html(title);
trigger(self.onHeaderCellRendered, {
"node": $header[0],
"column": columnDef
});
}
}
function getHeaderRow() {
return $headerRow[0];
}
function getHeaderRowColumn(columnId) {
var idx = getColumnIndex(columnId);
var $header = $headerRow.children().eq(idx);
return $header && $header[0];
}
function createColumnHeaders() {
function onMouseEnter() {
$(this).addClass("ui-state-hover");
}
function onMouseLeave() {
$(this).removeClass("ui-state-hover");
}
$headers.find(".slick-header-column")
.each(function() {
var columnDef = $(this).data("column");
if (columnDef) {
trigger(self.onBeforeHeaderCellDestroy, {
"node": this,
"column": columnDef
});
}
});
$headers.empty();
$headers.width(getHeadersWidth());
$headerRow.find(".slick-headerrow-column")
.each(function() {
var columnDef = $(this).data("column");
if (columnDef) {
trigger(self.onBeforeHeaderRowCellDestroy, {
"node": this,
"column": columnDef
});
}
});
$headerRow.empty();
for (var i = 0; i < columns.length; i++) {
var m = columns[i];
var header = $("<div class='ui-state-default slick-header-column' />")
.html("<span class='slick-column-name'>" + m.name + "</span>")
.width(m.width - headerColumnWidthDiff)
.attr("id", "" + uid + m.id)
.attr("title", m.toolTip || "")
.data("column", m)
.addClass(m.headerCssClass || "")
.appendTo($headers);
if (options.enableColumnReorder || m.sortable) {
header
.on('mouseenter', onMouseEnter)
.on('mouseleave', onMouseLeave);
}
if (m.sortable) {
header.addClass("slick-header-sortable");
header.append("<span class='slick-sort-indicator' />");
}
trigger(self.onHeaderCellRendered, {
"node": header[0],
"column": m
});
if (options.showHeaderRow) {
var headerRowCell = $("<div class='ui-state-default slick-headerrow-column l" + i + " r" + i + "'></div>")
.data("column", m)
.appendTo($headerRow);
trigger(self.onHeaderRowCellRendered, {
"node": headerRowCell[0],
"column": m
});
}
}
setSortColumns(sortColumns);
setupColumnResize();
if (options.enableColumnReorder) {
setupColumnReorder();
}
}
function setupColumnSort() {
$headers.click(function (e) {
// temporary workaround for a bug in jQuery 1.7.1 (http://bugs.jquery.com/ticket/11328)
e.metaKey = e.metaKey || e.ctrlKey;
if ($(e.target).hasClass("slick-resizable-handle")) {
return;
}
var $col = $(e.target).closest(".slick-header-column");
if (!$col.length) {
return;
}
var column = $col.data("column");
if (column.sortable) {
if (!getEditorLock().commitCurrentEdit()) {
return;
}
var sortOpts = null;
var i = 0;
for (; i < sortColumns.length; i++) {
if (sortColumns[i].columnId == column.id) {
sortOpts = sortColumns[i];
sortOpts.sortAsc = !sortOpts.sortAsc;
break;
}
}
if (e.metaKey && options.multiColumnSort) {
if (sortOpts) {
sortColumns.splice(i, 1);
}
}
else {
if ((!e.shiftKey && !e.metaKey) || !options.multiColumnSort) {
sortColumns = [];
}
if (!sortOpts) {
sortOpts = { columnId: column.id, sortAsc: column.defaultSortAsc };
sortColumns.push(sortOpts);
} else if (sortColumns.length == 0) {
sortColumns.push(sortOpts);
}
}
setSortColumns(sortColumns);
if (!options.multiColumnSort) {
trigger(self.onSort, {
multiColumnSort: false,
sortCol: column,
sortAsc: sortOpts.sortAsc}, e);
} else {
trigger(self.onSort, {
multiColumnSort: true,
sortCols: $.map(sortColumns, function(col) {
return {sortCol: columns[getColumnIndex(col.columnId)], sortAsc: col.sortAsc };
})}, e);
}
}
});
}
function setupColumnReorder() {
$headers.filter(":ui-sortable").sortable("destroy");
$headers.sortable({
containment: "parent",
distance: 3,
axis: "x",
cursor: "default",
tolerance: "intersection",
helper: "clone",
placeholder: "slick-sortable-placeholder ui-state-default slick-header-column",
start: function (e, ui) {
ui.placeholder.width(ui.helper.outerWidth() - headerColumnWidthDiff);
$(ui.helper).addClass("slick-header-column-active");
},
beforeStop: function (e, ui) {
$(ui.helper).removeClass("slick-header-column-active");
},
stop: function (e) {
if (!getEditorLock().commitCurrentEdit()) {
$(this).sortable("cancel");
return;
}
var reorderedIds = $headers.sortable("toArray");
var reorderedColumns = [];
for (var i = 0; i < reorderedIds.length; i++) {
reorderedColumns.push(columns[getColumnIndex(reorderedIds[i].replace(uid, ""))]);
}
setColumns(reorderedColumns);
trigger(self.onColumnsReordered, {});
e.stopPropagation();
setupColumnResize();
}
});
}
function setupColumnResize() {
var $col, j, c, pageX, columnElements, minPageX, maxPageX, firstResizable, lastResizable;
columnElements = $headers.children();
columnElements.find(".slick-resizable-handle").remove();
columnElements.each(function (i, e) {
if (columns[i].resizable) {
if (firstResizable === undefined) {
firstResizable = i;
}
lastResizable = i;
}
});
if (firstResizable === undefined) {
return;
}
columnElements.each(function (i, e) {
if (i < firstResizable || (options.forceFitColumns && i >= lastResizable)) {
return;
}
$col = $(e);
$("<div class='slick-resizable-handle' />")
.appendTo(e)
.bind("dragstart", function (e, dd) {
if (!getEditorLock().commitCurrentEdit()) {
return false;
}
pageX = e.pageX;
$(this).parent().addClass("slick-header-column-active");
var shrinkLeewayOnRight = null, stretchLeewayOnRight = null;
// lock each column's width option to current width
columnElements.each(function (i, e) {
columns[i].previousWidth = $(e).outerWidth();
});
if (options.forceFitColumns) {
shrinkLeewayOnRight = 0;
stretchLeewayOnRight = 0;
// colums on right affect maxPageX/minPageX
for (j = i + 1; j < columnElements.length; j++) {
c = columns[j];
if (c.resizable) {
if (stretchLeewayOnRight !== null) {
if (c.maxWidth) {
stretchLeewayOnRight += c.maxWidth - c.previousWidth;
} else {
stretchLeewayOnRight = null;
}
}
shrinkLeewayOnRight += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
}
}
}
var shrinkLeewayOnLeft = 0, stretchLeewayOnLeft = 0;
for (j = 0; j <= i; j++) {
// columns on left only affect minPageX
c = columns[j];
if (c.resizable) {
if (stretchLeewayOnLeft !== null) {
if (c.maxWidth) {
stretchLeewayOnLeft += c.maxWidth - c.previousWidth;
} else {
stretchLeewayOnLeft = null;
}
}
shrinkLeewayOnLeft += c.previousWidth - Math.max(c.minWidth || 0, absoluteColumnMinWidth);
}
}
if (shrinkLeewayOnRight === null) {
shrinkLeewayOnRight = 100000;
}
if (shrinkLeewayOnLeft === null) {
shrinkLeewayOnLeft = 100000;
}
if (stretchLeewayOnRight === null) {
stretchLeewayOnRight = 100000;
}
if (stretchLeewayOnLeft === null) {
stretchLeewayOnLeft = 100000;
}
maxPageX = pageX + Math.min(shrinkLeewayOnRight, stretchLeewayOnLeft);
minPageX = pageX - Math.min(shrinkLeewayOnLeft, stretchLeewayOnRight);
})
.bind("drag", function (e, dd) {
var actualMinWidth, d = Math.min(maxPageX, Math.max(minPageX, e.pageX)) - pageX, x;
if (d < 0) { // shrink column
x = d;
for (j = i; j >= 0; j--) {
c = columns[j];
if (c.resizable) {
actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
if (x && c.previousWidth + x < actualMinWidth) {
x += c.previousWidth - actualMinWidth;
c.width = actualMinWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
if (options.forceFitColumns) {
x = -d;
for (j = i + 1; j < columnElements.length; j++) {
c = columns[j];
if (c.resizable) {
if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
x -= c.maxWidth - c.previousWidth;
c.width = c.maxWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
}
} else { // stretch column
x = d;
for (j = i; j >= 0; j--) {
c = columns[j];
if (c.resizable) {
if (x && c.maxWidth && (c.maxWidth - c.previousWidth < x)) {
x -= c.maxWidth - c.previousWidth;
c.width = c.maxWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
if (options.forceFitColumns) {
x = -d;
for (j = i + 1; j < columnElements.length; j++) {
c = columns[j];
if (c.resizable) {
actualMinWidth = Math.max(c.minWidth || 0, absoluteColumnMinWidth);
if (x && c.previousWidth + x < actualMinWidth) {
x += c.previousWidth - actualMinWidth;
c.width = actualMinWidth;
} else {
c.width = c.previousWidth + x;
x = 0;
}
}
}
}
}
applyColumnHeaderWidths();
if (options.syncColumnCellResize) {
applyColumnWidths();
}
})
.bind("dragend", function (e, dd) {
var newWidth;
$(this).parent().removeClass("slick-header-column-active");
for (j = 0; j < columnElements.length; j++) {
c = columns[j];
newWidth = $(columnElements[j]).outerWidth();
if (c.previousWidth !== newWidth && c.rerenderOnResize) {
invalidateAllRows();
}
}
updateCanvasWidth(true);
render();
trigger(self.onColumnsResized, {});
});
});
}
function getVBoxDelta($el) {
var p = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
var delta = 0;
$.each(p, function (n, val) {
delta += parseFloat($el.css(val)) || 0;
});
return delta;
}
function measureCellPaddingAndBorder() {
var el;
var h = ["borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight"];
var v = ["borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom"];
el = $("<div class='ui-state-default slick-header-column' style='visibility:hidden'>-</div>").appendTo($headers);
headerColumnWidthDiff = headerColumnHeightDiff = 0;
if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
$.each(h, function (n, val) {
headerColumnWidthDiff += parseFloat(el.css(val)) || 0;
});
$.each(v, function (n, val) {
headerColumnHeightDiff += parseFloat(el.css(val)) || 0;
});
}
el.remove();
var r = $("<div class='slick-row' />").appendTo($canvas);
el = $("<div class='slick-cell' id='' style='visibility:hidden'>-</div>").appendTo(r);
cellWidthDiff = cellHeightDiff = 0;
if (el.css("box-sizing") != "border-box" && el.css("-moz-box-sizing") != "border-box" && el.css("-webkit-box-sizing") != "border-box") {
$.each(h, function (n, val) {
cellWidthDiff += parseFloat(el.css(val)) || 0;
});
$.each(v, function (n, val) {
cellHeightDiff += parseFloat(el.css(val)) || 0;
});
}
r.remove();
absoluteColumnMinWidth = Math.max(headerColumnWidthDiff, cellWidthDiff);
}
function createCssRules() {
$style = $("<style type='text/css' rel='stylesheet' />").appendTo($("head"));
var rowHeight = (options.rowHeight - cellHeightDiff);
var rules = [
"." + uid + " .slick-header-column { left: 1000px; }",
"." + uid + " .slick-top-panel { height:" + options.topPanelHeight + "px; }",
"." + uid + " .slick-headerrow-columns { height:" + options.headerRowHeight + "px; }",
"." + uid + " .slick-cell { height:" + rowHeight + "px; }",
"." + uid + " .slick-row { height:" + options.rowHeight + "px; }"
];
for (var i = 0; i < columns.length; i++) {
rules.push("." + uid + " .l" + i + " { }");
rules.push("." + uid + " .r" + i + " { }");
}
if ($style[0].styleSheet) { // IE
$style[0].styleSheet.cssText = rules.join(" ");
} else {
$style[0].appendChild(document.createTextNode(rules.join(" ")));
}
}
function getColumnCssRules(idx) {
if (!stylesheet) {
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
if ((sheets[i].ownerNode || sheets[i].owningElement) == $style[0]) {
stylesheet = sheets[i];
break;
}
}
if (!stylesheet) {
throw new Error("Cannot find stylesheet.");
}
// find and cache column CSS rules
columnCssRulesL = [];
columnCssRulesR = [];
var cssRules = (stylesheet.cssRules || stylesheet.rules);
var matches, columnIdx;
for (var i = 0; i < cssRules.length; i++) {
var selector = cssRules[i].selectorText;
if (matches = /\.l\d+/.exec(selector)) {
columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
columnCssRulesL[columnIdx] = cssRules[i];
} else if (matches = /\.r\d+/.exec(selector)) {
columnIdx = parseInt(matches[0].substr(2, matches[0].length - 2), 10);
columnCssRulesR[columnIdx] = cssRules[i];
}
}
}
return {
"left": columnCssRulesL[idx],
"right": columnCssRulesR[idx]
};
}
function removeCssRules() {
$style.remove();
stylesheet = null;
}
function destroy() {
getEditorLock().cancelCurrentEdit();
trigger(self.onBeforeDestroy, {});
var i = plugins.length;
while(i--) {
unregisterPlugin(plugins[i]);
}
if (options.enableColumnReorder) {
$headers.filter(":ui-sortable").sortable("destroy");
}
unbindAncestorScrollEvents();
$container.unbind(".slickgrid");
removeCssRules();
$canvas.unbind("draginit dragstart dragend drag");
$container.empty().removeClass(uid);
}
//////////////////////////////////////////////////////////////////////////////////////////////
// General
function trigger(evt, args, e) {
e = e || new Slick.EventData();
args = args || {};
args.grid = self;
return evt.notify(args, e, self);
}
function getEditorLock() {
return options.editorLock;
}
function getEditController() {
return editController;
}
function getColumnIndex(id) {
return columnsById[id];
}
function autosizeColumns() {
var i, c,
widths = [],
shrinkLeeway = 0,
total = 0,
prevTotal,
availWidth = viewportHasVScroll ? viewportW - scrollbarDimensions.width : viewportW;
for (i = 0; i < columns.length; i++) {
c = columns[i];
widths.push(c.width);
total += c.width;
if (c.resizable) {
shrinkLeeway += c.width - Math.max(c.minWidth, absoluteColumnMinWidth);
}
}
// shrink
prevTotal = total;
while (total > availWidth && shrinkLeeway) {
var shrinkProportion = (total - availWidth) / shrinkLeeway;
for (i = 0; i < columns.length && total > availWidth; i++) {
c = columns[i];
var width = widths[i];
if (!c.resizable || width <= c.minWidth || width <= absoluteColumnMinWidth) {
continue;
}
var absMinWidth = Math.max(c.minWidth, absoluteColumnMinWidth);
var shrinkSize = Math.floor(shrinkProportion * (width - absMinWidth)) || 1;
shrinkSize = Math.min(shrinkSize, width - absMinWidth);
total -= shrinkSize;
shrinkLeeway -= shrinkSize;
widths[i] -= shrinkSize;
}
if (prevTotal <= total) { // avoid infinite loop
break;
}
prevTotal = total;
}
// grow
prevTotal = total;
while (total < availWidth) {
var growProportion = availWidth / total;
for (i = 0; i < columns.length && total < availWidth; i++) {
c = columns[i];
var currentWidth = widths[i];
var growSize;
if (!c.resizable || c.maxWidth <= currentWidth) {
growSize = 0;
} else {
growSize = Math.min(Math.floor(growProportion * currentWidth) - currentWidth, (c.maxWidth - currentWidth) || 1000000) || 1;
}
total += growSize;
widths[i] += growSize;
}
if (prevTotal >= total) { // avoid infinite loop
break;
}
prevTotal = total;
}
var reRender = false;
for (i = 0; i < columns.length; i++) {
if (columns[i].rerenderOnResize && columns[i].width != widths[i]) {
reRender = true;
}
columns[i].width = widths[i];
}
applyColumnHeaderWidths();
updateCanvasWidth(true);
if (reRender) {
invalidateAllRows();
render();
}
}
function applyColumnHeaderWidths() {
if (!initialized) { return; }
var h;
for (var i = 0, headers = $headers.children(), ii = headers.length; i < ii; i++) {
h = $(headers[i]);
if (h.width() !== columns[i].width - headerColumnWidthDiff) {
h.width(columns[i].width - headerColumnWidthDiff);
}
}
updateColumnCaches();
}
function applyColumnWidths() {
var x = 0, w, rule;
for (var i = 0; i < columns.length; i++) {
w = columns[i].width;
rule = getColumnCssRules(i);
rule.left.style.left = x + "px";
rule.right.style.right = (canvasWidth - x - w) + "px";
x += columns[i].width;
}
}
function setSortColumn(columnId, ascending) {
setSortColumns([{ columnId: columnId, sortAsc: ascending}]);
}
function setSortColumns(cols) {
sortColumns = cols;
var headerColumnEls = $headers.children();
headerColumnEls
.removeClass("slick-header-column-sorted")
.find(".slick-sort-indicator")
.removeClass("slick-sort-indicator-asc slick-sort-indicator-desc");
$.each(sortColumns, function(i, col) {
if (col.sortAsc == null) {
col.sortAsc = true;
}
var columnIndex = getColumnIndex(col.columnId);
if (columnIndex != null) {
headerColumnEls.eq(columnIndex)
.addClass("slick-header-column-sorted")
.find(".slick-sort-indicator")
.addClass(col.sortAsc ? "slick-sort-indicator-asc" : "slick-sort-indicator-desc");
}
});
}
function getSortColumns() {
return sortColumns;
}
function handleSelectedRangesChanged(e, ranges) {
selectedRows = [];
var hash = {};
for (var i = 0; i < ranges.length; i++) {
for (var j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
if (!hash[j]) { // prevent duplicates
selectedRows.push(j);
hash[j] = {};
}
for (var k = ranges[i].fromCell; k <= ranges[i].toCell; k++) {
if (canCellBeSelected(j, k)) {
hash[j][columns[k].id] = options.selectedCellCssClass;
}
}
}
}
setCellCssStyles(options.selectedCellCssClass, hash);
trigger(self.onSelectedRowsChanged, {rows: getSelectedRows()}, e);
}
function getColumns() {
return columns;
}
function updateColumnCaches() {
// Pre-calculate cell boundaries.
columnPosLeft = [];
columnPosRight = [];
var x = 0;
for (var i = 0, ii = columns.length; i < ii; i++) {
columnPosLeft[i] = x;
columnPosRight[i] = x + columns[i].width;
x += columns[i].width;
}
}
function setColumns(columnDefinitions) {
columns = columnDefinitions;
columnsById = {};
for (var i = 0; i < columns.length; i++) {
var m = columns[i] = $.extend({}, columnDefaults, columns[i]);
columnsById[m.id] = i;
if (m.minWidth && m.width < m.minWidth) {
m.width = m.minWidth;
}
if (m.maxWidth && m.width > m.maxWidth) {
m.width = m.maxWidth;
}
}
updateColumnCaches();
if (initialized) {
invalidateAllRows();
createColumnHeaders();
removeCssRules();
createCssRules();
resizeCanvas();
applyColumnWidths();
handleScroll();
}
}
function getOptions() {
return options;
}
function setOptions(args) {
if (!getEditorLock().commitCurrentEdit()) {
return;
}
makeActiveCellNormal();
if (options.enableAddRow !== args.enableAddRow) {
invalidateRow(getDataLength());
}
options = $.extend(options, args);
validateAndEnforceOptions();
$viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto");
render();
}
function validateAndEnforceOptions() {
if (options.autoHeight) {
options.leaveSpaceForNewRows = false;
}
}
function setData(newData, scrollToTop) {
data = newData;
invalidateAllRows();
updateRowCount();
if (scrollToTop) {
scrollTo(0);
}
}
function getData() {
return data;
}
function getDataLength() {
if (data.getLength) {
return data.getLength();
} else {
return data.length;
}
}
function getDataLengthIncludingAddNew() {
return getDataLength() + (options.enableAddRow ? 1 : 0);
}
function getDataItem(i) {
if (data.getItem) {
return data.getItem(i);
} else {
return data[i];
}
}
function getTopPanel() {
return $topPanel[0];
}
function setTopPanelVisibility(visible) {
if (options.showTopPanel != visible) {
options.showTopPanel = visible;
if (visible) {
$topPanelScroller.slideDown("fast", resizeCanvas);
} else {
$topPanelScroller.slideUp("fast", resizeCanvas);
}
}
}
function setHeaderRowVisibility(visible) {
if (options.showHeaderRow != visible) {
options.showHeaderRow = visible;
if (visible) {
$headerRowScroller.slideDown("fast", resizeCanvas);
} else {
$headerRowScroller.slideUp("fast", resizeCanvas);
}
}
}
function getContainerNode() {
return $container.get(0);
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Rendering / Scrolling
function getRowTop(row) {
return options.rowHeight * row - offset;
}
function getRowFromPosition(y) {
return Math.floor((y + offset) / options.rowHeight);
}
function scrollTo(y) {
y = Math.max(y, 0);
y = Math.min(y, th - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0));
var oldOffset = offset;
page = Math.min(n - 1, Math.floor(y / ph));
offset = Math.round(page * cj);
var newScrollTop = y - offset;
if (offset != oldOffset) {
var range = getVisibleRange(newScrollTop);
cleanupRows(range);
updateRowPositions();
}
if (prevScrollTop != newScrollTop) {
vScrollDir = (prevScrollTop + oldOffset < newScrollTop + offset) ? 1 : -1;
$viewport[0].scrollTop = (lastRenderedScrollTop = scrollTop = prevScrollTop = newScrollTop);
trigger(self.onViewportChanged, {});
}
}
function defaultFormatter(row, cell, value, columnDef, dataContext) {
if (value == null) {
return "";
} else {
return (value + "").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
}
}
function getFormatter(row, column) {
var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
// look up by id, then index
var columnOverrides = rowMetadata &&
rowMetadata.columns &&
(rowMetadata.columns[column.id] || rowMetadata.columns[getColumnIndex(column.id)]);
return (columnOverrides && columnOverrides.formatter) ||
(rowMetadata && rowMetadata.formatter) ||
column.formatter ||
(options.formatterFactory && options.formatterFactory.getFormatter(column)) ||
options.defaultFormatter;
}
function getEditor(row, cell) {
var column = columns[cell];
var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
var columnMetadata = rowMetadata && rowMetadata.columns;
if (columnMetadata && columnMetadata[column.id] && columnMetadata[column.id].editor !== undefined) {
return columnMetadata[column.id].editor;
}
if (columnMetadata && columnMetadata[cell] && columnMetadata[cell].editor !== undefined) {
return columnMetadata[cell].editor;
}
return column.editor || (options.editorFactory && options.editorFactory.getEditor(column));
}
function getDataItemValueForColumn(item, columnDef) {
if (options.dataItemColumnValueExtractor) {
return options.dataItemColumnValueExtractor(item, columnDef);
}
return item[columnDef.field];
}
function appendRowHtml(stringArray, row, range, dataLength) {
var d = getDataItem(row);
var dataLoading = row < dataLength && !d;
var rowCss = "slick-row" +
(dataLoading ? " loading" : "") +
(row === activeRow ? " active" : "") +
(row % 2 == 1 ? " odd" : " even");
if (!d) {
rowCss += " " + options.addNewRowCssClass;
}
var metadata = data.getItemMetadata && data.getItemMetadata(row);
if (metadata && metadata.cssClasses) {
rowCss += " " + metadata.cssClasses;
}
stringArray.push("<div class='ui-widget-content " + rowCss + "' style='top:" + getRowTop(row) + "px'>");
var colspan, m;
for (var i = 0, ii = columns.length; i < ii; i++) {
m = columns[i];
colspan = 1;
if (metadata && metadata.columns) {
var columnData = metadata.columns[m.id] || metadata.columns[i];
colspan = (columnData && columnData.colspan) || 1;
if (colspan === "*") {
colspan = ii - i;
}
}
// Do not render cells outside of the viewport.
if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
if (columnPosLeft[i] > range.rightPx) {
// All columns to the right are outside the range.
break;
}
appendCellHtml(stringArray, row, i, colspan, d);
}
if (colspan > 1) {
i += (colspan - 1);
}
}
stringArray.push("</div>");
}
function appendCellHtml(stringArray, row, cell, colspan, item) {
var m = columns[cell];
var cellCss = "slick-cell l" + cell + " r" + Math.min(columns.length - 1, cell + colspan - 1) +
(m.cssClass ? " " + m.cssClass : "");
if (row === activeRow && cell === activeCell) {
cellCss += (" active");
}
// TODO: merge them together in the setter
for (var key in cellCssClasses) {
if (cellCssClasses[key][row] && cellCssClasses[key][row][m.id]) {
cellCss += (" " + cellCssClasses[key][row][m.id]);
}
}
stringArray.push("<div class='" + cellCss + "'>");
// if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet)
if (item) {
var value = getDataItemValueForColumn(item, m);
stringArray.push(getFormatter(row, m)(row, cell, value, m, item));
}
stringArray.push("</div>");
rowsCache[row].cellRenderQueue.push(cell);
rowsCache[row].cellColSpans[cell] = colspan;
}
function cleanupRows(rangeToKeep) {
for (var i in rowsCache) {
if (((i = parseInt(i, 10)) !== activeRow) && (i < rangeToKeep.top || i > rangeToKeep.bottom)) {
removeRowFromCache(i);
}
}
}
function invalidate() {
updateRowCount();
invalidateAllRows();
render();
}
function invalidateAllRows() {
if (currentEditor) {
makeActiveCellNormal();
}
for (var row in rowsCache) {
removeRowFromCache(row);
}
}
function removeRowFromCache(row) {
var cacheEntry = rowsCache[row];
if (!cacheEntry) {
return;
}
if (rowNodeFromLastMouseWheelEvent == cacheEntry.rowNode) {
cacheEntry.rowNode.style.display = 'none';
zombieRowNodeFromLastMouseWheelEvent = rowNodeFromLastMouseWheelEvent;
} else {
$canvas[0].removeChild(cacheEntry.rowNode);
}
delete rowsCache[row];
delete postProcessedRows[row];
renderedRows--;
counter_rows_removed++;
}
function invalidateRows(rows) {
var i, rl;
if (!rows || !rows.length) {
return;
}
vScrollDir = 0;
for (i = 0, rl = rows.length; i < rl; i++) {
if (currentEditor && activeRow === rows[i]) {
makeActiveCellNormal();
}
if (rowsCache[rows[i]]) {
removeRowFromCache(rows[i]);
}
}
}
function invalidateRow(row) {
invalidateRows([row]);
}
function updateCell(row, cell) {
var cellNode = getCellNode(row, cell);
if (!cellNode) {
return;
}
var m = columns[cell], d = getDataItem(row);
if (currentEditor && activeRow === row && activeCell === cell) {
currentEditor.loadValue(d);
} else {
cellNode.innerHTML = d ? getFormatter(row, m)(row, cell, getDataItemValueForColumn(d, m), m, d) : "";
invalidatePostProcessingResults(row);
}
}
function updateRow(row) {
var cacheEntry = rowsCache[row];
if (!cacheEntry) {
return;
}
ensureCellNodesInRowsCache(row);
var d = getDataItem(row);
for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
continue;
}
columnIdx = columnIdx | 0;
var m = columns[columnIdx],
node = cacheEntry.cellNodesByColumnIdx[columnIdx];
if (row === activeRow && columnIdx === activeCell && currentEditor) {
currentEditor.loadValue(d);
} else if (d) {
node.innerHTML = getFormatter(row, m)(row, columnIdx, getDataItemValueForColumn(d, m), m, d);
} else {
node.innerHTML = "";
}
}
invalidatePostProcessingResults(row);
}
function getViewportHeight() {
return parseFloat($.css($container[0], "height", true)) -
parseFloat($.css($container[0], "paddingTop", true)) -
parseFloat($.css($container[0], "paddingBottom", true)) -
parseFloat($.css($headerScroller[0], "height")) - getVBoxDelta($headerScroller) -
(options.showTopPanel ? options.topPanelHeight + getVBoxDelta($topPanelScroller) : 0) -
(options.showHeaderRow ? options.headerRowHeight + getVBoxDelta($headerRowScroller) : 0);
}
function resizeCanvas() {
if (!initialized) { return; }
if (options.autoHeight) {
viewportH = options.rowHeight * getDataLengthIncludingAddNew();
} else {
viewportH = getViewportHeight();
}
numVisibleRows = Math.ceil(viewportH / options.rowHeight);
viewportW = parseFloat($.css($container[0], "width", true));
if (!options.autoHeight) {
$viewport.height(viewportH);
}
if (options.forceFitColumns) {
autosizeColumns();
}
updateRowCount();
handleScroll();
// Since the width has changed, force the render() to reevaluate virtually rendered cells.
lastRenderedScrollLeft = -1;
render();
}
function updateRowCount() {
if (!initialized) { return; }
var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
var numberOfRows = dataLengthIncludingAddNew +
(options.leaveSpaceForNewRows ? numVisibleRows - 1 : 0);
var oldViewportHasVScroll = viewportHasVScroll;
// with autoHeight, we do not need to accommodate the vertical scroll bar
viewportHasVScroll = !options.autoHeight && (numberOfRows * options.rowHeight > viewportH);
makeActiveCellNormal();
// remove the rows that are now outside of the data range
// this helps avoid redundant calls to .removeRow() when the size of the data decreased by thousands of rows
var l = dataLengthIncludingAddNew - 1;
for (var i in rowsCache) {
if (i >= l) {
removeRowFromCache(i);
}
}
if (activeCellNode && activeRow > l) {
resetActiveCell();
}
var oldH = h;
th = Math.max(options.rowHeight * numberOfRows, viewportH - scrollbarDimensions.height);
if (th < maxSupportedCssHeight) {
// just one page
h = ph = th;
n = 1;
cj = 0;
} else {
// break into pages
h = maxSupportedCssHeight;
ph = h / 100;
n = Math.floor(th / ph);
cj = (th - h) / (n - 1);
}
if (h !== oldH) {
$canvas.css("height", h);
scrollTop = $viewport[0].scrollTop;
}
var oldScrollTopInRange = (scrollTop + offset <= th - viewportH);
if (th == 0 || scrollTop == 0) {
page = offset = 0;
} else if (oldScrollTopInRange) {
// maintain virtual position
scrollTo(scrollTop + offset);
} else {
// scroll to bottom
scrollTo(th - viewportH);
}
if (h != oldH && options.autoHeight) {
resizeCanvas();
}
if (options.forceFitColumns && oldViewportHasVScroll != viewportHasVScroll) {
autosizeColumns();
}
updateCanvasWidth(false);
}
function getVisibleRange(viewportTop, viewportLeft) {
if (viewportTop == null) {
viewportTop = scrollTop;
}
if (viewportLeft == null) {
viewportLeft = scrollLeft;
}
return {
top: getRowFromPosition(viewportTop),
bottom: getRowFromPosition(viewportTop + viewportH) + 1,
leftPx: viewportLeft,
rightPx: viewportLeft + viewportW
};
}
function getRenderedRange(viewportTop, viewportLeft) {
var range = getVisibleRange(viewportTop, viewportLeft);
var buffer = Math.round(viewportH / options.rowHeight);
var minBuffer = 3;
if (vScrollDir == -1) {
range.top -= buffer;
range.bottom += minBuffer;
} else if (vScrollDir == 1) {
range.top -= minBuffer;
range.bottom += buffer;
} else {
range.top -= minBuffer;
range.bottom += minBuffer;
}
range.top = Math.max(0, range.top);
range.bottom = Math.min(getDataLengthIncludingAddNew() - 1, range.bottom);
range.leftPx -= viewportW;
range.rightPx += viewportW;
range.leftPx = Math.max(0, range.leftPx);
range.rightPx = Math.min(canvasWidth, range.rightPx);
return range;
}
function ensureCellNodesInRowsCache(row) {
var cacheEntry = rowsCache[row];
if (cacheEntry) {
if (cacheEntry.cellRenderQueue.length) {
var lastChild = cacheEntry.rowNode.lastChild;
while (cacheEntry.cellRenderQueue.length) {
var columnIdx = cacheEntry.cellRenderQueue.pop();
cacheEntry.cellNodesByColumnIdx[columnIdx] = lastChild;
lastChild = lastChild.previousSibling;
}
}
}
}
function cleanUpCells(range, row) {
var totalCellsRemoved = 0;
var cacheEntry = rowsCache[row];
// Remove cells outside the range.
var cellsToRemove = [];
for (var i in cacheEntry.cellNodesByColumnIdx) {
// I really hate it when people mess with Array.prototype.
if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(i)) {
continue;
}
// This is a string, so it needs to be cast back to a number.
i = i | 0;
var colspan = cacheEntry.cellColSpans[i];
if (columnPosLeft[i] > range.rightPx ||
columnPosRight[Math.min(columns.length - 1, i + colspan - 1)] < range.leftPx) {
if (!(row == activeRow && i == activeCell)) {
cellsToRemove.push(i);
}
}
}
var cellToRemove;
while ((cellToRemove = cellsToRemove.pop()) != null) {
cacheEntry.rowNode.removeChild(cacheEntry.cellNodesByColumnIdx[cellToRemove]);
delete cacheEntry.cellColSpans[cellToRemove];
delete cacheEntry.cellNodesByColumnIdx[cellToRemove];
if (postProcessedRows[row]) {
delete postProcessedRows[row][cellToRemove];
}
totalCellsRemoved++;
}
}
function cleanUpAndRenderCells(range) {
var cacheEntry;
var stringArray = [];
var processedRows = [];
var cellsAdded;
var totalCellsAdded = 0;
var colspan;
for (var row = range.top, btm = range.bottom; row <= btm; row++) {
cacheEntry = rowsCache[row];
if (!cacheEntry) {
continue;
}
// cellRenderQueue populated in renderRows() needs to be cleared first
ensureCellNodesInRowsCache(row);
cleanUpCells(range, row);
// Render missing cells.
cellsAdded = 0;
var metadata = data.getItemMetadata && data.getItemMetadata(row);
metadata = metadata && metadata.columns;
var d = getDataItem(row);
// TODO: shorten this loop (index? heuristics? binary search?)
for (var i = 0, ii = columns.length; i < ii; i++) {
// Cells to the right are outside the range.
if (columnPosLeft[i] > range.rightPx) {
break;
}
// Already rendered.
if ((colspan = cacheEntry.cellColSpans[i]) != null) {
i += (colspan > 1 ? colspan - 1 : 0);
continue;
}
colspan = 1;
if (metadata) {
var columnData = metadata[columns[i].id] || metadata[i];
colspan = (columnData && columnData.colspan) || 1;
if (colspan === "*") {
colspan = ii - i;
}
}
if (columnPosRight[Math.min(ii - 1, i + colspan - 1)] > range.leftPx) {
appendCellHtml(stringArray, row, i, colspan, d);
cellsAdded++;
}
i += (colspan > 1 ? colspan - 1 : 0);
}
if (cellsAdded) {
totalCellsAdded += cellsAdded;
processedRows.push(row);
}
}
if (!stringArray.length) {
return;
}
var x = document.createElement("div");
x.innerHTML = stringArray.join("");
var processedRow;
var node;
while ((processedRow = processedRows.pop()) != null) {
cacheEntry = rowsCache[processedRow];
var columnIdx;
while ((columnIdx = cacheEntry.cellRenderQueue.pop()) != null) {
node = x.lastChild;
cacheEntry.rowNode.appendChild(node);
cacheEntry.cellNodesByColumnIdx[columnIdx] = node;
}
}
}
function renderRows(range) {
var parentNode = $canvas[0],
stringArray = [],
rows = [],
needToReselectCell = false,
dataLength = getDataLength();
for (var i = range.top, ii = range.bottom; i <= ii; i++) {
if (rowsCache[i]) {
continue;
}
renderedRows++;
rows.push(i);
// Create an entry right away so that appendRowHtml() can
// start populatating it.
rowsCache[i] = {
"rowNode": null,
// ColSpans of rendered cells (by column idx).
// Can also be used for checking whether a cell has been rendered.
"cellColSpans": [],
// Cell nodes (by column idx). Lazy-populated by ensureCellNodesInRowsCache().
"cellNodesByColumnIdx": [],
// Column indices of cell nodes that have been rendered, but not yet indexed in
// cellNodesByColumnIdx. These are in the same order as cell nodes added at the
// end of the row.
"cellRenderQueue": []
};
appendRowHtml(stringArray, i, range, dataLength);
if (activeCellNode && activeRow === i) {
needToReselectCell = true;
}
counter_rows_rendered++;
}
if (!rows.length) { return; }
var x = document.createElement("div");
x.innerHTML = stringArray.join("");
for (var i = 0, ii = rows.length; i < ii; i++) {
rowsCache[rows[i]].rowNode = parentNode.appendChild(x.firstChild);
}
if (needToReselectCell) {
activeCellNode = getCellNode(activeRow, activeCell);
}
}
function startPostProcessing() {
if (!options.enableAsyncPostRender) {
return;
}
clearTimeout(h_postrender);
h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
}
function invalidatePostProcessingResults(row) {
delete postProcessedRows[row];
postProcessFromRow = Math.min(postProcessFromRow, row);
postProcessToRow = Math.max(postProcessToRow, row);
startPostProcessing();
}
function updateRowPositions() {
for (var row in rowsCache) {
rowsCache[row].rowNode.style.top = getRowTop(row) + "px";
}
}
function render() {
if (!initialized) { return; }
var visible = getVisibleRange();
var rendered = getRenderedRange();
// remove rows no longer in the viewport
cleanupRows(rendered);
// add new rows & missing cells in existing rows
if (lastRenderedScrollLeft != scrollLeft) {
cleanUpAndRenderCells(rendered);
}
// render missing rows
renderRows(rendered);
postProcessFromRow = visible.top;
postProcessToRow = Math.min(getDataLengthIncludingAddNew() - 1, visible.bottom);
startPostProcessing();
lastRenderedScrollTop = scrollTop;
lastRenderedScrollLeft = scrollLeft;
h_render = null;
}
function handleHeaderRowScroll() {
var scrollLeft = $headerRowScroller[0].scrollLeft;
if (scrollLeft != $viewport[0].scrollLeft) {
$viewport[0].scrollLeft = scrollLeft;
}
}
function handleScroll() {
scrollTop = $viewport[0].scrollTop;
scrollLeft = $viewport[0].scrollLeft;
var vScrollDist = Math.abs(scrollTop - prevScrollTop);
var hScrollDist = Math.abs(scrollLeft - prevScrollLeft);
if (hScrollDist) {
prevScrollLeft = scrollLeft;
$headerScroller[0].scrollLeft = scrollLeft;
$topPanelScroller[0].scrollLeft = scrollLeft;
$headerRowScroller[0].scrollLeft = scrollLeft;
}
if (vScrollDist) {
vScrollDir = prevScrollTop < scrollTop ? 1 : -1;
prevScrollTop = scrollTop;
// switch virtual pages if needed
if (vScrollDist < viewportH) {
scrollTo(scrollTop + offset);
} else {
var oldOffset = offset;
if (h == viewportH) {
page = 0;
} else {
page = Math.min(n - 1, Math.floor(scrollTop * ((th - viewportH) / (h - viewportH)) * (1 / ph)));
}
offset = Math.round(page * cj);
if (oldOffset != offset) {
invalidateAllRows();
}
}
}
if (hScrollDist || vScrollDist) {
if (h_render) {
clearTimeout(h_render);
}
if (Math.abs(lastRenderedScrollTop - scrollTop) > 20 ||
Math.abs(lastRenderedScrollLeft - scrollLeft) > 20) {
if (options.forceSyncScrolling || (
Math.abs(lastRenderedScrollTop - scrollTop) < viewportH &&
Math.abs(lastRenderedScrollLeft - scrollLeft) < viewportW)) {
render();
} else {
h_render = setTimeout(render, 50);
}
trigger(self.onViewportChanged, {});
}
}
trigger(self.onScroll, {scrollLeft: scrollLeft, scrollTop: scrollTop});
}
function asyncPostProcessRows() {
var dataLength = getDataLength();
while (postProcessFromRow <= postProcessToRow) {
var row = (vScrollDir >= 0) ? postProcessFromRow++ : postProcessToRow--;
var cacheEntry = rowsCache[row];
if (!cacheEntry || row >= dataLength) {
continue;
}
if (!postProcessedRows[row]) {
postProcessedRows[row] = {};
}
ensureCellNodesInRowsCache(row);
for (var columnIdx in cacheEntry.cellNodesByColumnIdx) {
if (!cacheEntry.cellNodesByColumnIdx.hasOwnProperty(columnIdx)) {
continue;
}
columnIdx = columnIdx | 0;
var m = columns[columnIdx];
if (m.asyncPostRender && !postProcessedRows[row][columnIdx]) {
var node = cacheEntry.cellNodesByColumnIdx[columnIdx];
if (node) {
m.asyncPostRender(node, row, getDataItem(row), m);
}
postProcessedRows[row][columnIdx] = true;
}
}
h_postrender = setTimeout(asyncPostProcessRows, options.asyncPostRenderDelay);
return;
}
}
function updateCellCssStylesOnRenderedRows(addedHash, removedHash) {
var node, columnId, addedRowHash, removedRowHash;
for (var row in rowsCache) {
removedRowHash = removedHash && removedHash[row];
addedRowHash = addedHash && addedHash[row];
if (removedRowHash) {
for (columnId in removedRowHash) {
if (!addedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
node = getCellNode(row, getColumnIndex(columnId));
if (node) {
$(node).removeClass(removedRowHash[columnId]);
}
}
}
}
if (addedRowHash) {
for (columnId in addedRowHash) {
if (!removedRowHash || removedRowHash[columnId] != addedRowHash[columnId]) {
node = getCellNode(row, getColumnIndex(columnId));
if (node) {
$(node).addClass(addedRowHash[columnId]);
}
}
}
}
}
}
function addCellCssStyles(key, hash) {
if (cellCssClasses[key]) {
throw "addCellCssStyles: cell CSS hash with key '" + key + "' already exists.";
}
cellCssClasses[key] = hash;
updateCellCssStylesOnRenderedRows(hash, null);
trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
}
function removeCellCssStyles(key) {
if (!cellCssClasses[key]) {
return;
}
updateCellCssStylesOnRenderedRows(null, cellCssClasses[key]);
delete cellCssClasses[key];
trigger(self.onCellCssStylesChanged, { "key": key, "hash": null });
}
function setCellCssStyles(key, hash) {
var prevHash = cellCssClasses[key];
cellCssClasses[key] = hash;
updateCellCssStylesOnRenderedRows(hash, prevHash);
trigger(self.onCellCssStylesChanged, { "key": key, "hash": hash });
}
function getCellCssStyles(key) {
return cellCssClasses[key];
}
function flashCell(row, cell, speed) {
speed = speed || 100;
if (rowsCache[row]) {
var $cell = $(getCellNode(row, cell));
function toggleCellClass(times) {
if (!times) {
return;
}
setTimeout(function () {
$cell.queue(function () {
$cell.toggleClass(options.cellFlashingCssClass).dequeue();
toggleCellClass(times - 1);
});
},
speed);
}
toggleCellClass(4);
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Interactivity
function handleMouseWheel(e) {
var rowNode = $(e.target).closest(".slick-row")[0];
if (rowNode != rowNodeFromLastMouseWheelEvent) {
if (zombieRowNodeFromLastMouseWheelEvent && zombieRowNodeFromLastMouseWheelEvent != rowNode) {
$canvas[0].removeChild(zombieRowNodeFromLastMouseWheelEvent);
zombieRowNodeFromLastMouseWheelEvent = null;
}
rowNodeFromLastMouseWheelEvent = rowNode;
}
}
function handleDragInit(e, dd) {
var cell = getCellFromEvent(e);
if (!cell || !cellExists(cell.row, cell.cell)) {
return false;
}
var retval = trigger(self.onDragInit, dd, e);
if (e.isImmediatePropagationStopped()) {
return retval;
}
// if nobody claims to be handling drag'n'drop by stopping immediate propagation,
// cancel out of it
return false;
}
function handleDragStart(e, dd) {
var cell = getCellFromEvent(e);
if (!cell || !cellExists(cell.row, cell.cell)) {
return false;
}
var retval = trigger(self.onDragStart, dd, e);
if (e.isImmediatePropagationStopped()) {
return retval;
}
return false;
}
function handleDrag(e, dd) {
return trigger(self.onDrag, dd, e);
}
function handleDragEnd(e, dd) {
trigger(self.onDragEnd, dd, e);
}
function handleKeyDown(e) {
trigger(self.onKeyDown, {row: activeRow, cell: activeCell}, e);
var handled = e.isImmediatePropagationStopped();
if (!handled) {
if (!e.shiftKey && !e.altKey && !e.ctrlKey) {
if (e.which == 27) {
if (!getEditorLock().isActive()) {
return; // no editing mode to cancel, allow bubbling and default processing (exit without cancelling the event)
}
cancelEditAndSetFocus();
} else if (e.which == 34) {
navigatePageDown();
handled = true;
} else if (e.which == 33) {
navigatePageUp();
handled = true;
} else if (e.which == 37) {
handled = navigateLeft();
} else if (e.which == 39) {
handled = navigateRight();
} else if (e.which == 38) {
handled = navigateUp();
} else if (e.which == 40) {
handled = navigateDown();
} else if (e.which == 9) {
handled = navigateNext();
} else if (e.which == 13) {
if (options.editable) {
if (currentEditor) {
// adding new row
if (activeRow === getDataLength()) {
navigateDown();
} else {
commitEditAndSetFocus();
}
} else {
if (getEditorLock().commitCurrentEdit()) {
makeActiveCellEditable();
}
}
}
handled = true;
}
} else if (e.which == 9 && e.shiftKey && !e.ctrlKey && !e.altKey) {
handled = navigatePrev();
}
}
if (handled) {
// the event has been handled so don't let parent element (bubbling/propagation) or browser (default) handle it
e.stopPropagation();
e.preventDefault();
try {
e.originalEvent.keyCode = 0; // prevent default behaviour for special keys in IE browsers (F3, F5, etc.)
}
// ignore exceptions - setting the original event's keycode throws access denied exception for "Ctrl"
// (hitting control key only, nothing else), "Shift" (maybe others)
catch (error) {
}
}
}
function handleClick(e) {
if (!currentEditor) {
// if this click resulted in some cell child node getting focus,
// don't steal it back - keyboard events will still bubble up
// IE9+ seems to default DIVs to tabIndex=0 instead of -1, so check for cell clicks directly.
if (e.target != document.activeElement || $(e.target).hasClass("slick-cell")) {
setFocus();
}
}
var cell = getCellFromEvent(e);
if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
return;
}
trigger(self.onClick, {row: cell.row, cell: cell.cell}, e);
if (e.isImmediatePropagationStopped()) {
return;
}
if ((activeCell != cell.cell || activeRow != cell.row) && canCellBeActive(cell.row, cell.cell)) {
if (!getEditorLock().isActive() || getEditorLock().commitCurrentEdit()) {
scrollRowIntoView(cell.row, false);
setActiveCellInternal(getCellNode(cell.row, cell.cell));
}
}
}
function handleContextMenu(e) {
var $cell = $(e.target).closest(".slick-cell", $canvas);
if ($cell.length === 0) {
return;
}
// are we editing this cell?
if (activeCellNode === $cell[0] && currentEditor !== null) {
return;
}
trigger(self.onContextMenu, {}, e);
}
function handleDblClick(e) {
var cell = getCellFromEvent(e);
if (!cell || (currentEditor !== null && activeRow == cell.row && activeCell == cell.cell)) {
return;
}
trigger(self.onDblClick, {row: cell.row, cell: cell.cell}, e);
if (e.isImmediatePropagationStopped()) {
return;
}
if (options.editable) {
gotoCell(cell.row, cell.cell, true);
}
}
function handleHeaderMouseEnter(e) {
trigger(self.onHeaderMouseEnter, {
"column": $(this).data("column")
}, e);
}
function handleHeaderMouseLeave(e) {
trigger(self.onHeaderMouseLeave, {
"column": $(this).data("column")
}, e);
}
function handleHeaderContextMenu(e) {
var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
var column = $header && $header.data("column");
trigger(self.onHeaderContextMenu, {column: column}, e);
}
function handleHeaderClick(e) {
var $header = $(e.target).closest(".slick-header-column", ".slick-header-columns");
var column = $header && $header.data("column");
if (column) {
trigger(self.onHeaderClick, {column: column}, e);
}
}
function handleMouseEnter(e) {
trigger(self.onMouseEnter, {}, e);
}
function handleMouseLeave(e) {
trigger(self.onMouseLeave, {}, e);
}
function cellExists(row, cell) {
return !(row < 0 || row >= getDataLength() || cell < 0 || cell >= columns.length);
}
function getCellFromPoint(x, y) {
var row = getRowFromPosition(y);
var cell = 0;
var w = 0;
for (var i = 0; i < columns.length && w < x; i++) {
w += columns[i].width;
cell++;
}
if (cell < 0) {
cell = 0;
}
return {row: row, cell: cell - 1};
}
function getCellFromNode(cellNode) {
// read column number from .l<columnNumber> CSS class
var cls = /l\d+/.exec(cellNode.className);
if (!cls) {
throw "getCellFromNode: cannot get cell - " + cellNode.className;
}
return parseInt(cls[0].substr(1, cls[0].length - 1), 10);
}
function getRowFromNode(rowNode) {
for (var row in rowsCache) {
if (rowsCache[row].rowNode === rowNode) {
return row | 0;
}
}
return null;
}
function getCellFromEvent(e) {
var $cell = $(e.target).closest(".slick-cell", $canvas);
if (!$cell.length) {
return null;
}
var row = getRowFromNode($cell[0].parentNode);
var cell = getCellFromNode($cell[0]);
if (row == null || cell == null) {
return null;
} else {
return {
"row": row,
"cell": cell
};
}
}
function getCellNodeBox(row, cell) {
if (!cellExists(row, cell)) {
return null;
}
var y1 = getRowTop(row);
var y2 = y1 + options.rowHeight - 1;
var x1 = 0;
for (var i = 0; i < cell; i++) {
x1 += columns[i].width;
}
var x2 = x1 + columns[cell].width;
return {
top: y1,
left: x1,
bottom: y2,
right: x2
};
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Cell switching
function resetActiveCell() {
setActiveCellInternal(null, false);
}
function setFocus() {
if (tabbingDirection == -1) {
$focusSink[0].focus();
} else {
$focusSink2[0].focus();
}
}
function scrollCellIntoView(row, cell, doPaging) {
scrollRowIntoView(row, doPaging);
var colspan = getColspan(row, cell);
var left = columnPosLeft[cell],
right = columnPosRight[cell + (colspan > 1 ? colspan - 1 : 0)],
scrollRight = scrollLeft + viewportW;
if (left < scrollLeft) {
$viewport.scrollLeft(left);
handleScroll();
render();
} else if (right > scrollRight) {
$viewport.scrollLeft(Math.min(left, right - $viewport[0].clientWidth));
handleScroll();
render();
}
}
function setActiveCellInternal(newCell, opt_editMode) {
if (activeCellNode !== null) {
makeActiveCellNormal();
$(activeCellNode).removeClass("active");
if (rowsCache[activeRow]) {
$(rowsCache[activeRow].rowNode).removeClass("active");
}
}
var activeCellChanged = (activeCellNode !== newCell);
activeCellNode = newCell;
if (activeCellNode != null) {
activeRow = getRowFromNode(activeCellNode.parentNode);
activeCell = activePosX = getCellFromNode(activeCellNode);
if (opt_editMode == null) {
opt_editMode = (activeRow == getDataLength()) || options.autoEdit;
}
$(activeCellNode).addClass("active");
$(rowsCache[activeRow].rowNode).addClass("active");
if (options.editable && opt_editMode && isCellPotentiallyEditable(activeRow, activeCell)) {
clearTimeout(h_editorLoader);
if (options.asyncEditorLoading) {
h_editorLoader = setTimeout(function () {
makeActiveCellEditable();
}, options.asyncEditorLoadDelay);
} else {
makeActiveCellEditable();
}
}
} else {
activeRow = activeCell = null;
}
if (activeCellChanged) {
trigger(self.onActiveCellChanged, getActiveCell());
}
}
function clearTextSelection() {
if (document.selection && document.selection.empty) {
try {
//IE fails here if selected element is not in dom
document.selection.empty();
} catch (e) { }
} else if (window.getSelection) {
var sel = window.getSelection();
if (sel && sel.removeAllRanges) {
sel.removeAllRanges();
}
}
}
function isCellPotentiallyEditable(row, cell) {
var dataLength = getDataLength();
// is the data for this row loaded?
if (row < dataLength && !getDataItem(row)) {
return false;
}
// are we in the Add New row? can we create new from this cell?
if (columns[cell].cannotTriggerInsert && row >= dataLength) {
return false;
}
// does this cell have an editor?
if (!getEditor(row, cell)) {
return false;
}
return true;
}
function makeActiveCellNormal() {
if (!currentEditor) {
return;
}
trigger(self.onBeforeCellEditorDestroy, {editor: currentEditor});
currentEditor.destroy();
currentEditor = null;
if (activeCellNode) {
var d = getDataItem(activeRow);
$(activeCellNode).removeClass("editable invalid");
if (d) {
var column = columns[activeCell];
var formatter = getFormatter(activeRow, column);
activeCellNode.innerHTML = formatter(activeRow, activeCell, getDataItemValueForColumn(d, column), column, d);
invalidatePostProcessingResults(activeRow);
}
}
// if there previously was text selected on a page (such as selected text in the edit cell just removed),
// IE can't set focus to anything else correctly
if (navigator.userAgent.toLowerCase().match(/msie/)) {
clearTextSelection();
}
getEditorLock().deactivate(editController);
}
function makeActiveCellEditable(editor) {
if (!activeCellNode) {
return;
}
if (!options.editable) {
throw "Grid : makeActiveCellEditable : should never get called when options.editable is false";
}
// cancel pending async call if there is one
clearTimeout(h_editorLoader);
if (!isCellPotentiallyEditable(activeRow, activeCell)) {
return;
}
var columnDef = columns[activeCell];
var item = getDataItem(activeRow);
if (trigger(self.onBeforeEditCell, {row: activeRow, cell: activeCell, item: item, column: columnDef}) === false) {
setFocus();
return;
}
getEditorLock().activate(editController);
$(activeCellNode).addClass("editable");
// don't clear the cell if a custom editor is passed through
if (!editor) {
activeCellNode.innerHTML = "";
}
currentEditor = new (editor || getEditor(activeRow, activeCell))({
grid: self,
gridPosition: absBox($container[0]),
position: absBox(activeCellNode),
container: activeCellNode,
column: columnDef,
item: item || {},
commitChanges: commitEditAndSetFocus,
cancelChanges: cancelEditAndSetFocus
});
if (item) {
currentEditor.loadValue(item);
}
serializedEditorValue = currentEditor.serializeValue();
if (currentEditor.position) {
handleActiveCellPositionChange();
}
}
function commitEditAndSetFocus() {
// if the commit fails, it would do so due to a validation error
// if so, do not steal the focus from the editor
if (getEditorLock().commitCurrentEdit()) {
setFocus();
if (options.autoEdit) {
navigateDown();
}
}
}
function cancelEditAndSetFocus() {
if (getEditorLock().cancelCurrentEdit()) {
setFocus();
}
}
function absBox(elem) {
var box = {
top: elem.offsetTop,
left: elem.offsetLeft,
bottom: 0,
right: 0,
width: $(elem).outerWidth(),
height: $(elem).outerHeight(),
visible: true};
box.bottom = box.top + box.height;
box.right = box.left + box.width;
// walk up the tree
var offsetParent = elem.offsetParent;
while ((elem = elem.parentNode) != document.body) {
if (box.visible && elem.scrollHeight != elem.offsetHeight && $(elem).css("overflowY") != "visible") {
box.visible = box.bottom > elem.scrollTop && box.top < elem.scrollTop + elem.clientHeight;
}
if (box.visible && elem.scrollWidth != elem.offsetWidth && $(elem).css("overflowX") != "visible") {
box.visible = box.right > elem.scrollLeft && box.left < elem.scrollLeft + elem.clientWidth;
}
box.left -= elem.scrollLeft;
box.top -= elem.scrollTop;
if (elem === offsetParent) {
box.left += elem.offsetLeft;
box.top += elem.offsetTop;
offsetParent = elem.offsetParent;
}
box.bottom = box.top + box.height;
box.right = box.left + box.width;
}
return box;
}
function getActiveCellPosition() {
return absBox(activeCellNode);
}
function getGridPosition() {
return absBox($container[0])
}
function handleActiveCellPositionChange() {
if (!activeCellNode) {
return;
}
trigger(self.onActiveCellPositionChanged, {});
if (currentEditor) {
var cellBox = getActiveCellPosition();
if (currentEditor.show && currentEditor.hide) {
if (!cellBox.visible) {
currentEditor.hide();
} else {
currentEditor.show();
}
}
if (currentEditor.position) {
currentEditor.position(cellBox);
}
}
}
function getCellEditor() {
return currentEditor;
}
function getActiveCell() {
if (!activeCellNode) {
return null;
} else {
return {row: activeRow, cell: activeCell};
}
}
function getActiveCellNode() {
return activeCellNode;
}
function scrollRowIntoView(row, doPaging) {
var rowAtTop = row * options.rowHeight;
var rowAtBottom = (row + 1) * options.rowHeight - viewportH + (viewportHasHScroll ? scrollbarDimensions.height : 0);
// need to page down?
if ((row + 1) * options.rowHeight > scrollTop + viewportH + offset) {
scrollTo(doPaging ? rowAtTop : rowAtBottom);
render();
}
// or page up?
else if (row * options.rowHeight < scrollTop + offset) {
scrollTo(doPaging ? rowAtBottom : rowAtTop);
render();
}
}
function scrollRowToTop(row) {
scrollTo(row * options.rowHeight);
render();
}
function scrollPage(dir) {
var deltaRows = dir * numVisibleRows;
scrollTo((getRowFromPosition(scrollTop) + deltaRows) * options.rowHeight);
render();
if (options.enableCellNavigation && activeRow != null) {
var row = activeRow + deltaRows;
var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
if (row >= dataLengthIncludingAddNew) {
row = dataLengthIncludingAddNew - 1;
}
if (row < 0) {
row = 0;
}
var cell = 0, prevCell = null;
var prevActivePosX = activePosX;
while (cell <= activePosX) {
if (canCellBeActive(row, cell)) {
prevCell = cell;
}
cell += getColspan(row, cell);
}
if (prevCell !== null) {
setActiveCellInternal(getCellNode(row, prevCell));
activePosX = prevActivePosX;
} else {
resetActiveCell();
}
}
}
function navigatePageDown() {
scrollPage(1);
}
function navigatePageUp() {
scrollPage(-1);
}
function getColspan(row, cell) {
var metadata = data.getItemMetadata && data.getItemMetadata(row);
if (!metadata || !metadata.columns) {
return 1;
}
var columnData = metadata.columns[columns[cell].id] || metadata.columns[cell];
var colspan = (columnData && columnData.colspan);
if (colspan === "*") {
colspan = columns.length - cell;
} else {
colspan = colspan || 1;
}
return colspan;
}
function findFirstFocusableCell(row) {
var cell = 0;
while (cell < columns.length) {
if (canCellBeActive(row, cell)) {
return cell;
}
cell += getColspan(row, cell);
}
return null;
}
function findLastFocusableCell(row) {
var cell = 0;
var lastFocusableCell = null;
while (cell < columns.length) {
if (canCellBeActive(row, cell)) {
lastFocusableCell = cell;
}
cell += getColspan(row, cell);
}
return lastFocusableCell;
}
function gotoRight(row, cell, posX) {
if (cell >= columns.length) {
return null;
}
do {
cell += getColspan(row, cell);
}
while (cell < columns.length && !canCellBeActive(row, cell));
if (cell < columns.length) {
return {
"row": row,
"cell": cell,
"posX": cell
};
}
return null;
}
function gotoLeft(row, cell, posX) {
if (cell <= 0) {
return null;
}
var firstFocusableCell = findFirstFocusableCell(row);
if (firstFocusableCell === null || firstFocusableCell >= cell) {
return null;
}
var prev = {
"row": row,
"cell": firstFocusableCell,
"posX": firstFocusableCell
};
var pos;
while (true) {
pos = gotoRight(prev.row, prev.cell, prev.posX);
if (!pos) {
return null;
}
if (pos.cell >= cell) {
return prev;
}
prev = pos;
}
}
function gotoDown(row, cell, posX) {
var prevCell;
var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
while (true) {
if (++row >= dataLengthIncludingAddNew) {
return null;
}
prevCell = cell = 0;
while (cell <= posX) {
prevCell = cell;
cell += getColspan(row, cell);
}
if (canCellBeActive(row, prevCell)) {
return {
"row": row,
"cell": prevCell,
"posX": posX
};
}
}
}
function gotoUp(row, cell, posX) {
var prevCell;
while (true) {
if (--row < 0) {
return null;
}
prevCell = cell = 0;
while (cell <= posX) {
prevCell = cell;
cell += getColspan(row, cell);
}
if (canCellBeActive(row, prevCell)) {
return {
"row": row,
"cell": prevCell,
"posX": posX
};
}
}
}
function gotoNext(row, cell, posX) {
if (row == null && cell == null) {
row = cell = posX = 0;
if (canCellBeActive(row, cell)) {
return {
"row": row,
"cell": cell,
"posX": cell
};
}
}
var pos = gotoRight(row, cell, posX);
if (pos) {
return pos;
}
var firstFocusableCell = null;
var dataLengthIncludingAddNew = getDataLengthIncludingAddNew();
while (++row < dataLengthIncludingAddNew) {
firstFocusableCell = findFirstFocusableCell(row);
if (firstFocusableCell !== null) {
return {
"row": row,
"cell": firstFocusableCell,
"posX": firstFocusableCell
};
}
}
return null;
}
function gotoPrev(row, cell, posX) {
if (row == null && cell == null) {
row = getDataLengthIncludingAddNew() - 1;
cell = posX = columns.length - 1;
if (canCellBeActive(row, cell)) {
return {
"row": row,
"cell": cell,
"posX": cell
};
}
}
var pos;
var lastSelectableCell;
while (!pos) {
pos = gotoLeft(row, cell, posX);
if (pos) {
break;
}
if (--row < 0) {
return null;
}
cell = 0;
lastSelectableCell = findLastFocusableCell(row);
if (lastSelectableCell !== null) {
pos = {
"row": row,
"cell": lastSelectableCell,
"posX": lastSelectableCell
};
}
}
return pos;
}
function navigateRight() {
return navigate("right");
}
function navigateLeft() {
return navigate("left");
}
function navigateDown() {
return navigate("down");
}
function navigateUp() {
return navigate("up");
}
function navigateNext() {
return navigate("next");
}
function navigatePrev() {
return navigate("prev");
}
/**
* @param {string} dir Navigation direction.
* @return {boolean} Whether navigation resulted in a change of active cell.
*/
function navigate(dir) {
if (!options.enableCellNavigation) {
return false;
}
if (!activeCellNode && dir != "prev" && dir != "next") {
return false;
}
if (!getEditorLock().commitCurrentEdit()) {
return true;
}
setFocus();
var tabbingDirections = {
"up": -1,
"down": 1,
"left": -1,
"right": 1,
"prev": -1,
"next": 1
};
tabbingDirection = tabbingDirections[dir];
var stepFunctions = {
"up": gotoUp,
"down": gotoDown,
"left": gotoLeft,
"right": gotoRight,
"prev": gotoPrev,
"next": gotoNext
};
var stepFn = stepFunctions[dir];
var pos = stepFn(activeRow, activeCell, activePosX);
if (pos) {
var isAddNewRow = (pos.row == getDataLength());
scrollCellIntoView(pos.row, pos.cell, !isAddNewRow);
setActiveCellInternal(getCellNode(pos.row, pos.cell));
activePosX = pos.posX;
return true;
} else {
setActiveCellInternal(getCellNode(activeRow, activeCell));
return false;
}
}
function getCellNode(row, cell) {
if (rowsCache[row]) {
ensureCellNodesInRowsCache(row);
return rowsCache[row].cellNodesByColumnIdx[cell];
}
return null;
}
function setActiveCell(row, cell) {
if (!initialized) { return; }
if (row > getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
return;
}
if (!options.enableCellNavigation) {
return;
}
scrollCellIntoView(row, cell, false);
setActiveCellInternal(getCellNode(row, cell), false);
}
function canCellBeActive(row, cell) {
if (!options.enableCellNavigation || row >= getDataLengthIncludingAddNew() ||
row < 0 || cell >= columns.length || cell < 0) {
return false;
}
var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
if (rowMetadata && typeof rowMetadata.focusable === "boolean") {
return rowMetadata.focusable;
}
var columnMetadata = rowMetadata && rowMetadata.columns;
if (columnMetadata && columnMetadata[columns[cell].id] && typeof columnMetadata[columns[cell].id].focusable === "boolean") {
return columnMetadata[columns[cell].id].focusable;
}
if (columnMetadata && columnMetadata[cell] && typeof columnMetadata[cell].focusable === "boolean") {
return columnMetadata[cell].focusable;
}
return columns[cell].focusable;
}
function canCellBeSelected(row, cell) {
if (row >= getDataLength() || row < 0 || cell >= columns.length || cell < 0) {
return false;
}
var rowMetadata = data.getItemMetadata && data.getItemMetadata(row);
if (rowMetadata && typeof rowMetadata.selectable === "boolean") {
return rowMetadata.selectable;
}
var columnMetadata = rowMetadata && rowMetadata.columns && (rowMetadata.columns[columns[cell].id] || rowMetadata.columns[cell]);
if (columnMetadata && typeof columnMetadata.selectable === "boolean") {
return columnMetadata.selectable;
}
return columns[cell].selectable;
}
function gotoCell(row, cell, forceEdit) {
if (!initialized) { return; }
if (!canCellBeActive(row, cell)) {
return;
}
if (!getEditorLock().commitCurrentEdit()) {
return;
}
scrollCellIntoView(row, cell, false);
var newCell = getCellNode(row, cell);
// if selecting the 'add new' row, start editing right away
setActiveCellInternal(newCell, forceEdit || (row === getDataLength()) || options.autoEdit);
// if no editor was created, set the focus back on the grid
if (!currentEditor) {
setFocus();
}
}
//////////////////////////////////////////////////////////////////////////////////////////////
// IEditor implementation for the editor lock
function commitCurrentEdit() {
var item = getDataItem(activeRow);
var column = columns[activeCell];
if (currentEditor) {
if (currentEditor.isValueChanged()) {
var validationResults = currentEditor.validate();
if (validationResults.valid) {
if (activeRow < getDataLength()) {
var editCommand = {
row: activeRow,
cell: activeCell,
editor: currentEditor,
serializedValue: currentEditor.serializeValue(),
prevSerializedValue: serializedEditorValue,
execute: function () {
this.editor.applyValue(item, this.serializedValue);
updateRow(this.row);
trigger(self.onCellChange, {
row: activeRow,
cell: activeCell,
item: item
});
},
undo: function () {
this.editor.applyValue(item, this.prevSerializedValue);
updateRow(this.row);
trigger(self.onCellChange, {
row: activeRow,
cell: activeCell,
item: item
});
}
};
if (options.editCommandHandler) {
makeActiveCellNormal();
options.editCommandHandler(item, column, editCommand);
} else {
editCommand.execute();
makeActiveCellNormal();
}
} else {
var newItem = {};
currentEditor.applyValue(newItem, currentEditor.serializeValue());
makeActiveCellNormal();
trigger(self.onAddNewRow, {item: newItem, column: column});
}
// check whether the lock has been re-acquired by event handlers
return !getEditorLock().isActive();
} else {
// Re-add the CSS class to trigger transitions, if any.
$(activeCellNode).removeClass("invalid");
$(activeCellNode).width(); // force layout
$(activeCellNode).addClass("invalid");
trigger(self.onValidationError, {
editor: currentEditor,
cellNode: activeCellNode,
validationResults: validationResults,
row: activeRow,
cell: activeCell,
column: column
});
currentEditor.focus();
return false;
}
}
makeActiveCellNormal();
}
return true;
}
function cancelCurrentEdit() {
makeActiveCellNormal();
return true;
}
function rowsToRanges(rows) {
var ranges = [];
var lastCell = columns.length - 1;
for (var i = 0; i < rows.length; i++) {
ranges.push(new Slick.Range(rows[i], 0, rows[i], lastCell));
}
return ranges;
}
function getSelectedRows() {
if (!selectionModel) {
throw "Selection model is not set";
}
return selectedRows;
}
function setSelectedRows(rows) {
if (!selectionModel) {
throw "Selection model is not set";
}
selectionModel.setSelectedRanges(rowsToRanges(rows));
}
//////////////////////////////////////////////////////////////////////////////////////////////
// Debug
this.debug = function () {
var s = "";
s += ("\n" + "counter_rows_rendered: " + counter_rows_rendered);
s += ("\n" + "counter_rows_removed: " + counter_rows_removed);
s += ("\n" + "renderedRows: " + renderedRows);
s += ("\n" + "numVisibleRows: " + numVisibleRows);
s += ("\n" + "maxSupportedCssHeight: " + maxSupportedCssHeight);
s += ("\n" + "n(umber of pages): " + n);
s += ("\n" + "(current) page: " + page);
s += ("\n" + "page height (ph): " + ph);
s += ("\n" + "vScrollDir: " + vScrollDir);
alert(s);
};
// a debug helper to be able to access private members
this.eval = function (expr) {
return eval(expr);
};
//////////////////////////////////////////////////////////////////////////////////////////////
// Public API
$.extend(this, {
"slickGridVersion": "2.1",
// Events
"onScroll": new Slick.Event(),
"onSort": new Slick.Event(),
"onHeaderMouseEnter": new Slick.Event(),
"onHeaderMouseLeave": new Slick.Event(),
"onHeaderContextMenu": new Slick.Event(),
"onHeaderClick": new Slick.Event(),
"onHeaderCellRendered": new Slick.Event(),
"onBeforeHeaderCellDestroy": new Slick.Event(),
"onHeaderRowCellRendered": new Slick.Event(),
"onBeforeHeaderRowCellDestroy": new Slick.Event(),
"onMouseEnter": new Slick.Event(),
"onMouseLeave": new Slick.Event(),
"onClick": new Slick.Event(),
"onDblClick": new Slick.Event(),
"onContextMenu": new Slick.Event(),
"onKeyDown": new Slick.Event(),
"onAddNewRow": new Slick.Event(),
"onValidationError": new Slick.Event(),
"onViewportChanged": new Slick.Event(),
"onColumnsReordered": new Slick.Event(),
"onColumnsResized": new Slick.Event(),
"onCellChange": new Slick.Event(),
"onBeforeEditCell": new Slick.Event(),
"onBeforeCellEditorDestroy": new Slick.Event(),
"onBeforeDestroy": new Slick.Event(),
"onActiveCellChanged": new Slick.Event(),
"onActiveCellPositionChanged": new Slick.Event(),
"onDragInit": new Slick.Event(),
"onDragStart": new Slick.Event(),
"onDrag": new Slick.Event(),
"onDragEnd": new Slick.Event(),
"onSelectedRowsChanged": new Slick.Event(),
"onCellCssStylesChanged": new Slick.Event(),
// Methods
"registerPlugin": registerPlugin,
"unregisterPlugin": unregisterPlugin,
"getColumns": getColumns,
"setColumns": setColumns,
"getColumnIndex": getColumnIndex,
"updateColumnHeader": updateColumnHeader,
"setSortColumn": setSortColumn,
"setSortColumns": setSortColumns,
"getSortColumns": getSortColumns,
"autosizeColumns": autosizeColumns,
"getOptions": getOptions,
"setOptions": setOptions,
"getData": getData,
"getDataLength": getDataLength,
"getDataItem": getDataItem,
"setData": setData,
"getSelectionModel": getSelectionModel,
"setSelectionModel": setSelectionModel,
"getSelectedRows": getSelectedRows,
"setSelectedRows": setSelectedRows,
"getContainerNode": getContainerNode,
"render": render,
"invalidate": invalidate,
"invalidateRow": invalidateRow,
"invalidateRows": invalidateRows,
"invalidateAllRows": invalidateAllRows,
"updateCell": updateCell,
"updateRow": updateRow,
"getViewport": getVisibleRange,
"getRenderedRange": getRenderedRange,
"resizeCanvas": resizeCanvas,
"updateRowCount": updateRowCount,
"scrollRowIntoView": scrollRowIntoView,
"scrollRowToTop": scrollRowToTop,
"scrollCellIntoView": scrollCellIntoView,
"getCanvasNode": getCanvasNode,
"focus": setFocus,
"getCellFromPoint": getCellFromPoint,
"getCellFromEvent": getCellFromEvent,
"getActiveCell": getActiveCell,
"setActiveCell": setActiveCell,
"getActiveCellNode": getActiveCellNode,
"getActiveCellPosition": getActiveCellPosition,
"resetActiveCell": resetActiveCell,
"editActiveCell": makeActiveCellEditable,
"getCellEditor": getCellEditor,
"getCellNode": getCellNode,
"getCellNodeBox": getCellNodeBox,
"canCellBeSelected": canCellBeSelected,
"canCellBeActive": canCellBeActive,
"navigatePrev": navigatePrev,
"navigateNext": navigateNext,
"navigateUp": navigateUp,
"navigateDown": navigateDown,
"navigateLeft": navigateLeft,
"navigateRight": navigateRight,
"navigatePageUp": navigatePageUp,
"navigatePageDown": navigatePageDown,
"gotoCell": gotoCell,
"getTopPanel": getTopPanel,
"setTopPanelVisibility": setTopPanelVisibility,
"setHeaderRowVisibility": setHeaderRowVisibility,
"getHeaderRow": getHeaderRow,
"getHeaderRowColumn": getHeaderRowColumn,
"getGridPosition": getGridPosition,
"flashCell": flashCell,
"addCellCssStyles": addCellCssStyles,
"setCellCssStyles": setCellCssStyles,
"removeCellCssStyles": removeCellCssStyles,
"getCellCssStyles": getCellCssStyles,
"init": finishInitialization,
"destroy": destroy,
// IEditor implementation
"getEditorLock": getEditorLock,
"getEditController": getEditController
});
init();
}
}(jQuery));
(function ($) {
$.extend(true, window, {
Slick: {
Data: {
DataView: DataView,
Aggregators: {
Avg: AvgAggregator,
Min: MinAggregator,
Max: MaxAggregator,
Sum: SumAggregator
}
}
}
});
/***
* A sample Model implementation.
* Provides a filtered view of the underlying data.
*
* Relies on the data item having an "id" property uniquely identifying it.
*/
function DataView(options) {
var self = this;
var defaults = {
groupItemMetadataProvider: null,
inlineFilters: false
};
// private
var idProperty = "id"; // property holding a unique row id
var items = []; // data by index
var rows = []; // data by row
var idxById = {}; // indexes by id
var rowsById = null; // rows by id; lazy-calculated
var filter = null; // filter function
var updated = null; // updated item ids
var suspend = false; // suspends the recalculation
var sortAsc = true;
var fastSortField;
var sortComparer;
var refreshHints = {};
var prevRefreshHints = {};
var filterArgs;
var filteredItems = [];
var compiledFilter;
var compiledFilterWithCaching;
var filterCache = [];
// grouping
var groupingInfoDefaults = {
getter: null,
formatter: null,
comparer: function(a, b) { return a.value - b.value; },
predefinedValues: [],
aggregators: [],
aggregateEmpty: false,
aggregateCollapsed: false,
aggregateChildGroups: false,
collapsed: false,
displayTotalsRow: true,
lazyTotalsCalculation: false
};
var groupingInfos = [];
var groups = [];
var toggledGroupsByLevel = [];
var groupingDelimiter = ':|:';
var pagesize = 0;
var pagenum = 0;
var totalRows = 0;
// events
var onRowCountChanged = new Slick.Event();
var onRowsChanged = new Slick.Event();
var onPagingInfoChanged = new Slick.Event();
options = $.extend(true, {}, defaults, options);
function beginUpdate() {
suspend = true;
}
function endUpdate() {
suspend = false;
refresh();
}
function setRefreshHints(hints) {
refreshHints = hints;
}
function setFilterArgs(args) {
filterArgs = args;
}
function updateIdxById(startingIndex) {
startingIndex = startingIndex || 0;
var id;
for (var i = startingIndex, l = items.length; i < l; i++) {
id = items[i][idProperty];
if (id === undefined) {
throw "Each data element must implement a unique 'id' property";
}
idxById[id] = i;
}
}
function ensureIdUniqueness() {
var id;
for (var i = 0, l = items.length; i < l; i++) {
id = items[i][idProperty];
if (id === undefined || idxById[id] !== i) {
throw "Each data element must implement a unique 'id' property";
}
}
}
function getItems() {
return items;
}
function setItems(data, objectIdProperty) {
if (objectIdProperty !== undefined) {
idProperty = objectIdProperty;
}
items = filteredItems = data;
idxById = {};
updateIdxById();
ensureIdUniqueness();
refresh();
}
function setPagingOptions(args) {
if (args.pageSize != undefined) {
pagesize = args.pageSize;
pagenum = pagesize ? Math.min(pagenum, Math.max(0, Math.ceil(totalRows / pagesize) - 1)) : 0;
}
if (args.pageNum != undefined) {
pagenum = Math.min(args.pageNum, Math.max(0, Math.ceil(totalRows / pagesize) - 1));
}
onPagingInfoChanged.notify(getPagingInfo(), null, self);
refresh();
}
function getPagingInfo() {
var totalPages = pagesize ? Math.max(1, Math.ceil(totalRows / pagesize)) : 1;
return {pageSize: pagesize, pageNum: pagenum, totalRows: totalRows, totalPages: totalPages};
}
function sort(comparer, ascending) {
sortAsc = ascending;
sortComparer = comparer;
fastSortField = null;
if (ascending === false) {
items.reverse();
}
items.sort(comparer);
if (ascending === false) {
items.reverse();
}
idxById = {};
updateIdxById();
refresh();
}
/***
* Provides a workaround for the extremely slow sorting in IE.
* Does a [lexicographic] sort on a give column by temporarily overriding Object.prototype.toString
* to return the value of that field and then doing a native Array.sort().
*/
function fastSort(field, ascending) {
sortAsc = ascending;
fastSortField = field;
sortComparer = null;
var oldToString = Object.prototype.toString;
Object.prototype.toString = (typeof field == "function") ? field : function () {
return this[field]
};
// an extra reversal for descending sort keeps the sort stable
// (assuming a stable native sort implementation, which isn't true in some cases)
if (ascending === false) {
items.reverse();
}
items.sort();
Object.prototype.toString = oldToString;
if (ascending === false) {
items.reverse();
}
idxById = {};
updateIdxById();
refresh();
}
function reSort() {
if (sortComparer) {
sort(sortComparer, sortAsc);
} else if (fastSortField) {
fastSort(fastSortField, sortAsc);
}
}
function setFilter(filterFn) {
filter = filterFn;
if (options.inlineFilters) {
compiledFilter = compileFilter();
compiledFilterWithCaching = compileFilterWithCaching();
}
refresh();
}
function getGrouping() {
return groupingInfos;
}
function setGrouping(groupingInfo) {
if (!options.groupItemMetadataProvider) {
options.groupItemMetadataProvider = new Slick.Data.GroupItemMetadataProvider();
}
groups = [];
toggledGroupsByLevel = [];
groupingInfo = groupingInfo || [];
groupingInfos = (groupingInfo instanceof Array) ? groupingInfo : [groupingInfo];
for (var i = 0; i < groupingInfos.length; i++) {
var gi = groupingInfos[i] = $.extend(true, {}, groupingInfoDefaults, groupingInfos[i]);
gi.getterIsAFn = typeof gi.getter === "function";
// pre-compile accumulator loops
gi.compiledAccumulators = [];
var idx = gi.aggregators.length;
while (idx--) {
gi.compiledAccumulators[idx] = compileAccumulatorLoop(gi.aggregators[idx]);
}
toggledGroupsByLevel[i] = {};
}
refresh();
}
/**
* @deprecated Please use {@link setGrouping}.
*/
function groupBy(valueGetter, valueFormatter, sortComparer) {
if (valueGetter == null) {
setGrouping([]);
return;
}
setGrouping({
getter: valueGetter,
formatter: valueFormatter,
comparer: sortComparer
});
}
/**
* @deprecated Please use {@link setGrouping}.
*/
function setAggregators(groupAggregators, includeCollapsed) {
if (!groupingInfos.length) {
throw new Error("At least one grouping must be specified before calling setAggregators().");
}
groupingInfos[0].aggregators = groupAggregators;
groupingInfos[0].aggregateCollapsed = includeCollapsed;
setGrouping(groupingInfos);
}
function getItemByIdx(i) {
return items[i];
}
function getIdxById(id) {
return idxById[id];
}
function ensureRowsByIdCache() {
if (!rowsById) {
rowsById = {};
for (var i = 0, l = rows.length; i < l; i++) {
rowsById[rows[i][idProperty]] = i;
}
}
}
function getRowById(id) {
ensureRowsByIdCache();
return rowsById[id];
}
function getItemById(id) {
return items[idxById[id]];
}
function mapIdsToRows(idArray) {
var rows = [];
ensureRowsByIdCache();
for (var i = 0, l = idArray.length; i < l; i++) {
var row = rowsById[idArray[i]];
if (row != null) {
rows[rows.length] = row;
}
}
return rows;
}
function mapRowsToIds(rowArray) {
var ids = [];
for (var i = 0, l = rowArray.length; i < l; i++) {
if (rowArray[i] < rows.length) {
ids[ids.length] = rows[rowArray[i]][idProperty];
}
}
return ids;
}
function updateItem(id, item) {
if (idxById[id] === undefined || id !== item[idProperty]) {
throw "Invalid or non-matching id";
}
items[idxById[id]] = item;
if (!updated) {
updated = {};
}
updated[id] = true;
refresh();
}
function insertItem(insertBefore, item) {
items.splice(insertBefore, 0, item);
updateIdxById(insertBefore);
refresh();
}
function addItem(item) {
items.push(item);
updateIdxById(items.length - 1);
refresh();
}
function deleteItem(id) {
var idx = idxById[id];
if (idx === undefined) {
throw "Invalid id";
}
delete idxById[id];
items.splice(idx, 1);
updateIdxById(idx);
refresh();
}
function getLength() {
return rows.length;
}
function getItem(i) {
var item = rows[i];
// if this is a group row, make sure totals are calculated and update the title
if (item && item.__group && item.totals && !item.totals.initialized) {
var gi = groupingInfos[item.level];
if (!gi.displayTotalsRow) {
calculateTotals(item.totals);
item.title = gi.formatter ? gi.formatter(item) : item.value;
}
}
// if this is a totals row, make sure it's calculated
else if (item && item.__groupTotals && !item.initialized) {
calculateTotals(item);
}
return item;
}
function getItemMetadata(i) {
var item = rows[i];
if (item === undefined) {
return null;
}
// overrides for grouping rows
if (item.__group) {
return options.groupItemMetadataProvider.getGroupRowMetadata(item);
}
// overrides for totals rows
if (item.__groupTotals) {
return options.groupItemMetadataProvider.getTotalsRowMetadata(item);
}
return null;
}
function expandCollapseAllGroups(level, collapse) {
if (level == null) {
for (var i = 0; i < groupingInfos.length; i++) {
toggledGroupsByLevel[i] = {};
groupingInfos[i].collapsed = collapse;
}
} else {
toggledGroupsByLevel[level] = {};
groupingInfos[level].collapsed = collapse;
}
refresh();
}
/**
* @param level {Number} Optional level to collapse. If not specified, applies to all levels.
*/
function collapseAllGroups(level) {
expandCollapseAllGroups(level, true);
}
/**
* @param level {Number} Optional level to expand. If not specified, applies to all levels.
*/
function expandAllGroups(level) {
expandCollapseAllGroups(level, false);
}
function expandCollapseGroup(level, groupingKey, collapse) {
toggledGroupsByLevel[level][groupingKey] = groupingInfos[level].collapsed ^ collapse;
refresh();
}
/**
* @param varArgs Either a Slick.Group's "groupingKey" property, or a
* variable argument list of grouping values denoting a unique path to the row. For
* example, calling collapseGroup('high', '10%') will collapse the '10%' subgroup of
* the 'high' group.
*/
function collapseGroup(varArgs) {
var args = Array.prototype.slice.call(arguments);
var arg0 = args[0];
if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, true);
} else {
expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), true);
}
}
/**
* @param varArgs Either a Slick.Group's "groupingKey" property, or a
* variable argument list of grouping values denoting a unique path to the row. For
* example, calling expandGroup('high', '10%') will expand the '10%' subgroup of
* the 'high' group.
*/
function expandGroup(varArgs) {
var args = Array.prototype.slice.call(arguments);
var arg0 = args[0];
if (args.length == 1 && arg0.indexOf(groupingDelimiter) != -1) {
expandCollapseGroup(arg0.split(groupingDelimiter).length - 1, arg0, false);
} else {
expandCollapseGroup(args.length - 1, args.join(groupingDelimiter), false);
}
}
function getGroups() {
return groups;
}
function extractGroups(rows, parentGroup) {
var group;
var val;
var groups = [];
var groupsByVal = {};
var r;
var level = parentGroup ? parentGroup.level + 1 : 0;
var gi = groupingInfos[level];
for (var i = 0, l = gi.predefinedValues.length; i < l; i++) {
val = gi.predefinedValues[i];
group = groupsByVal[val];
if (!group) {
group = new Slick.Group();
group.value = val;
group.level = level;
group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
groups[groups.length] = group;
groupsByVal[val] = group;
}
}
for (var i = 0, l = rows.length; i < l; i++) {
r = rows[i];
val = gi.getterIsAFn ? gi.getter(r) : r[gi.getter];
group = groupsByVal[val];
if (!group) {
group = new Slick.Group();
group.value = val;
group.level = level;
group.groupingKey = (parentGroup ? parentGroup.groupingKey + groupingDelimiter : '') + val;
groups[groups.length] = group;
groupsByVal[val] = group;
}
group.rows[group.count++] = r;
}
if (level < groupingInfos.length - 1) {
for (var i = 0; i < groups.length; i++) {
group = groups[i];
group.groups = extractGroups(group.rows, group);
}
}
groups.sort(groupingInfos[level].comparer);
return groups;
}
function calculateTotals(totals) {
var group = totals.group;
var gi = groupingInfos[group.level];
var isLeafLevel = (group.level == groupingInfos.length);
var agg, idx = gi.aggregators.length;
if (!isLeafLevel && gi.aggregateChildGroups) {
// make sure all the subgroups are calculated
var i = group.groups.length;
while (i--) {
if (!group.groups[i].initialized) {
calculateTotals(group.groups[i]);
}
}
}
while (idx--) {
agg = gi.aggregators[idx];
agg.init();
if (!isLeafLevel && gi.aggregateChildGroups) {
gi.compiledAccumulators[idx].call(agg, group.groups);
} else {
gi.compiledAccumulators[idx].call(agg, group.rows);
}
agg.storeResult(totals);
}
totals.initialized = true;
}
function addGroupTotals(group) {
var gi = groupingInfos[group.level];
var totals = new Slick.GroupTotals();
totals.group = group;
group.totals = totals;
if (!gi.lazyTotalsCalculation) {
calculateTotals(totals);
}
}
function addTotals(groups, level) {
level = level || 0;
var gi = groupingInfos[level];
var groupCollapsed = gi.collapsed;
var toggledGroups = toggledGroupsByLevel[level];
var idx = groups.length, g;
while (idx--) {
g = groups[idx];
if (g.collapsed && !gi.aggregateCollapsed) {
continue;
}
// Do a depth-first aggregation so that parent group aggregators can access subgroup totals.
if (g.groups) {
addTotals(g.groups, level + 1);
}
if (gi.aggregators.length && (
gi.aggregateEmpty || g.rows.length || (g.groups && g.groups.length))) {
addGroupTotals(g);
}
g.collapsed = groupCollapsed ^ toggledGroups[g.groupingKey];
g.title = gi.formatter ? gi.formatter(g) : g.value;
}
}
function flattenGroupedRows(groups, level) {
level = level || 0;
var gi = groupingInfos[level];
var groupedRows = [], rows, gl = 0, g;
for (var i = 0, l = groups.length; i < l; i++) {
g = groups[i];
groupedRows[gl++] = g;
if (!g.collapsed) {
rows = g.groups ? flattenGroupedRows(g.groups, level + 1) : g.rows;
for (var j = 0, jj = rows.length; j < jj; j++) {
groupedRows[gl++] = rows[j];
}
}
if (g.totals && gi.displayTotalsRow && (!g.collapsed || gi.aggregateCollapsed)) {
groupedRows[gl++] = g.totals;
}
}
return groupedRows;
}
function getFunctionInfo(fn) {
var fnRegex = /^function[^(]*\(([^)]*)\)\s*{([\s\S]*)}$/;
var matches = fn.toString().match(fnRegex);
return {
params: matches[1].split(","),
body: matches[2]
};
}
function compileAccumulatorLoop(aggregator) {
var accumulatorInfo = getFunctionInfo(aggregator.accumulate);
var fn = new Function(
"_items",
"for (var " + accumulatorInfo.params[0] + ", _i=0, _il=_items.length; _i<_il; _i++) {" +
accumulatorInfo.params[0] + " = _items[_i]; " +
accumulatorInfo.body +
"}"
);
fn.displayName = "compiledAccumulatorLoop";
return fn;
}
function compileFilter() {
var filterInfo = getFunctionInfo(filter);
var filterBody = filterInfo.body
.replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
.replace(/return true\s*([;}]|$)/gi, "{ _retval[_idx++] = $item$; continue _coreloop; }$1")
.replace(/return ([^;}]+?)\s*([;}]|$)/gi,
"{ if ($1) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
// This preserves the function template code after JS compression,
// so that replace() commands still work as expected.
var tpl = [
//"function(_items, _args) { ",
"var _retval = [], _idx = 0; ",
"var $item$, $args$ = _args; ",
"_coreloop: ",
"for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
"$item$ = _items[_i]; ",
"$filter$; ",
"} ",
"return _retval; "
//"}"
].join("");
tpl = tpl.replace(/\$filter\$/gi, filterBody);
tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
var fn = new Function("_items,_args", tpl);
fn.displayName = "compiledFilter";
return fn;
}
function compileFilterWithCaching() {
var filterInfo = getFunctionInfo(filter);
var filterBody = filterInfo.body
.replace(/return false\s*([;}]|$)/gi, "{ continue _coreloop; }$1")
.replace(/return true\s*([;}]|$)/gi, "{ _cache[_i] = true;_retval[_idx++] = $item$; continue _coreloop; }$1")
.replace(/return ([^;}]+?)\s*([;}]|$)/gi,
"{ if ((_cache[_i] = $1)) { _retval[_idx++] = $item$; }; continue _coreloop; }$2");
// This preserves the function template code after JS compression,
// so that replace() commands still work as expected.
var tpl = [
//"function(_items, _args, _cache) { ",
"var _retval = [], _idx = 0; ",
"var $item$, $args$ = _args; ",
"_coreloop: ",
"for (var _i = 0, _il = _items.length; _i < _il; _i++) { ",
"$item$ = _items[_i]; ",
"if (_cache[_i]) { ",
"_retval[_idx++] = $item$; ",
"continue _coreloop; ",
"} ",
"$filter$; ",
"} ",
"return _retval; "
//"}"
].join("");
tpl = tpl.replace(/\$filter\$/gi, filterBody);
tpl = tpl.replace(/\$item\$/gi, filterInfo.params[0]);
tpl = tpl.replace(/\$args\$/gi, filterInfo.params[1]);
var fn = new Function("_items,_args,_cache", tpl);
fn.displayName = "compiledFilterWithCaching";
return fn;
}
function uncompiledFilter(items, args) {
var retval = [], idx = 0;
for (var i = 0, ii = items.length; i < ii; i++) {
if (filter(items[i], args)) {
retval[idx++] = items[i];
}
}
return retval;
}
function uncompiledFilterWithCaching(items, args, cache) {
var retval = [], idx = 0, item;
for (var i = 0, ii = items.length; i < ii; i++) {
item = items[i];
if (cache[i]) {
retval[idx++] = item;
} else if (filter(item, args)) {
retval[idx++] = item;
cache[i] = true;
}
}
return retval;
}
function getFilteredAndPagedItems(items) {
if (filter) {
var batchFilter = options.inlineFilters ? compiledFilter : uncompiledFilter;
var batchFilterWithCaching = options.inlineFilters ? compiledFilterWithCaching : uncompiledFilterWithCaching;
if (refreshHints.isFilterNarrowing) {
filteredItems = batchFilter(filteredItems, filterArgs);
} else if (refreshHints.isFilterExpanding) {
filteredItems = batchFilterWithCaching(items, filterArgs, filterCache);
} else if (!refreshHints.isFilterUnchanged) {
filteredItems = batchFilter(items, filterArgs);
}
} else {
// special case: if not filtering and not paging, the resulting
// rows collection needs to be a copy so that changes due to sort
// can be caught
filteredItems = pagesize ? items : items.concat();
}
// get the current page
var paged;
if (pagesize) {
if (filteredItems.length < pagenum * pagesize) {
pagenum = Math.floor(filteredItems.length / pagesize);
}
paged = filteredItems.slice(pagesize * pagenum, pagesize * pagenum + pagesize);
} else {
paged = filteredItems;
}
return {totalRows: filteredItems.length, rows: paged};
}
function getRowDiffs(rows, newRows) {
var item, r, eitherIsNonData, diff = [];
var from = 0, to = newRows.length;
if (refreshHints && refreshHints.ignoreDiffsBefore) {
from = Math.max(0,
Math.min(newRows.length, refreshHints.ignoreDiffsBefore));
}
if (refreshHints && refreshHints.ignoreDiffsAfter) {
to = Math.min(newRows.length,
Math.max(0, refreshHints.ignoreDiffsAfter));
}
for (var i = from, rl = rows.length; i < to; i++) {
if (i >= rl) {
diff[diff.length] = i;
} else {
item = newRows[i];
r = rows[i];
if ((groupingInfos.length && (eitherIsNonData = (item.__nonDataRow) || (r.__nonDataRow)) &&
item.__group !== r.__group ||
item.__group && !item.equals(r))
|| (eitherIsNonData &&
// no good way to compare totals since they are arbitrary DTOs
// deep object comparison is pretty expensive
// always considering them 'dirty' seems easier for the time being
(item.__groupTotals || r.__groupTotals))
|| item[idProperty] != r[idProperty]
|| (updated && updated[item[idProperty]])
) {
diff[diff.length] = i;
}
}
}
return diff;
}
function recalc(_items) {
rowsById = null;
if (refreshHints.isFilterNarrowing != prevRefreshHints.isFilterNarrowing ||
refreshHints.isFilterExpanding != prevRefreshHints.isFilterExpanding) {
filterCache = [];
}
var filteredItems = getFilteredAndPagedItems(_items);
totalRows = filteredItems.totalRows;
var newRows = filteredItems.rows;
groups = [];
if (groupingInfos.length) {
groups = extractGroups(newRows);
if (groups.length) {
addTotals(groups);
newRows = flattenGroupedRows(groups);
}
}
var diff = getRowDiffs(rows, newRows);
rows = newRows;
return diff;
}
function refresh() {
if (suspend) {
return;
}
var countBefore = rows.length;
var totalRowsBefore = totalRows;
var diff = recalc(items, filter); // pass as direct refs to avoid closure perf hit
// if the current page is no longer valid, go to last page and recalc
// we suffer a performance penalty here, but the main loop (recalc) remains highly optimized
if (pagesize && totalRows < pagenum * pagesize) {
pagenum = Math.max(0, Math.ceil(totalRows / pagesize) - 1);
diff = recalc(items, filter);
}
updated = null;
prevRefreshHints = refreshHints;
refreshHints = {};
if (totalRowsBefore != totalRows) {
onPagingInfoChanged.notify(getPagingInfo(), null, self);
}
if (countBefore != rows.length) {
onRowCountChanged.notify({previous: countBefore, current: rows.length}, null, self);
}
if (diff.length > 0) {
onRowsChanged.notify({rows: diff}, null, self);
}
}
/***
* Wires the grid and the DataView together to keep row selection tied to item ids.
* This is useful since, without it, the grid only knows about rows, so if the items
* move around, the same rows stay selected instead of the selection moving along
* with the items.
*
* NOTE: This doesn't work with cell selection model.
*
* @param grid {Slick.Grid} The grid to sync selection with.
* @param preserveHidden {Boolean} Whether to keep selected items that go out of the
* view due to them getting filtered out.
* @param preserveHiddenOnSelectionChange {Boolean} Whether to keep selected items
* that are currently out of the view (see preserveHidden) as selected when selection
* changes.
* @return {Slick.Event} An event that notifies when an internal list of selected row ids
* changes. This is useful since, in combination with the above two options, it allows
* access to the full list selected row ids, and not just the ones visible to the grid.
* @method syncGridSelection
*/
function syncGridSelection(grid, preserveHidden, preserveHiddenOnSelectionChange) {
var self = this;
var inHandler;
var selectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
var onSelectedRowIdsChanged = new Slick.Event();
function setSelectedRowIds(rowIds) {
if (selectedRowIds.join(",") == rowIds.join(",")) {
return;
}
selectedRowIds = rowIds;
onSelectedRowIdsChanged.notify({
"grid": grid,
"ids": selectedRowIds
}, new Slick.EventData(), self);
}
function update() {
if (selectedRowIds.length > 0) {
inHandler = true;
var selectedRows = self.mapIdsToRows(selectedRowIds);
if (!preserveHidden) {
setSelectedRowIds(self.mapRowsToIds(selectedRows));
}
grid.setSelectedRows(selectedRows);
inHandler = false;
}
}
grid.onSelectedRowsChanged.subscribe(function(e, args) {
if (inHandler) { return; }
var newSelectedRowIds = self.mapRowsToIds(grid.getSelectedRows());
if (!preserveHiddenOnSelectionChange || !grid.getOptions().multiSelect) {
setSelectedRowIds(newSelectedRowIds);
} else {
// keep the ones that are hidden
var existing = $.grep(selectedRowIds, function(id) { return self.getRowById(id) === undefined; });
// add the newly selected ones
setSelectedRowIds(existing.concat(newSelectedRowIds));
}
});
this.onRowsChanged.subscribe(update);
this.onRowCountChanged.subscribe(update);
return onSelectedRowIdsChanged;
}
function syncGridCellCssStyles(grid, key) {
var hashById;
var inHandler;
// since this method can be called after the cell styles have been set,
// get the existing ones right away
storeCellCssStyles(grid.getCellCssStyles(key));
function storeCellCssStyles(hash) {
hashById = {};
for (var row in hash) {
var id = rows[row][idProperty];
hashById[id] = hash[row];
}
}
function update() {
if (hashById) {
inHandler = true;
ensureRowsByIdCache();
var newHash = {};
for (var id in hashById) {
var row = rowsById[id];
if (row != undefined) {
newHash[row] = hashById[id];
}
}
grid.setCellCssStyles(key, newHash);
inHandler = false;
}
}
grid.onCellCssStylesChanged.subscribe(function(e, args) {
if (inHandler) { return; }
if (key != args.key) { return; }
if (args.hash) {
storeCellCssStyles(args.hash);
}
});
this.onRowsChanged.subscribe(update);
this.onRowCountChanged.subscribe(update);
}
$.extend(this, {
// methods
"beginUpdate": beginUpdate,
"endUpdate": endUpdate,
"setPagingOptions": setPagingOptions,
"getPagingInfo": getPagingInfo,
"getItems": getItems,
"setItems": setItems,
"setFilter": setFilter,
"sort": sort,
"fastSort": fastSort,
"reSort": reSort,
"setGrouping": setGrouping,
"getGrouping": getGrouping,
"groupBy": groupBy,
"setAggregators": setAggregators,
"collapseAllGroups": collapseAllGroups,
"expandAllGroups": expandAllGroups,
"collapseGroup": collapseGroup,
"expandGroup": expandGroup,
"getGroups": getGroups,
"getIdxById": getIdxById,
"getRowById": getRowById,
"getItemById": getItemById,
"getItemByIdx": getItemByIdx,
"mapRowsToIds": mapRowsToIds,
"mapIdsToRows": mapIdsToRows,
"setRefreshHints": setRefreshHints,
"setFilterArgs": setFilterArgs,
"refresh": refresh,
"updateItem": updateItem,
"insertItem": insertItem,
"addItem": addItem,
"deleteItem": deleteItem,
"syncGridSelection": syncGridSelection,
"syncGridCellCssStyles": syncGridCellCssStyles,
// data provider methods
"getLength": getLength,
"getItem": getItem,
"getItemMetadata": getItemMetadata,
// events
"onRowCountChanged": onRowCountChanged,
"onRowsChanged": onRowsChanged,
"onPagingInfoChanged": onPagingInfoChanged
});
}
function AvgAggregator(field) {
this.field_ = field;
this.init = function () {
this.count_ = 0;
this.nonNullCount_ = 0;
this.sum_ = 0;
};
this.accumulate = function (item) {
var val = item[this.field_];
this.count_++;
if (val != null && val !== "" && val !== NaN) {
this.nonNullCount_++;
this.sum_ += parseFloat(val);
}
};
this.storeResult = function (groupTotals) {
if (!groupTotals.avg) {
groupTotals.avg = {};
}
if (this.nonNullCount_ != 0) {
groupTotals.avg[this.field_] = this.sum_ / this.nonNullCount_;
}
};
}
function MinAggregator(field) {
this.field_ = field;
this.init = function () {
this.min_ = null;
};
this.accumulate = function (item) {
var val = item[this.field_];
if (val != null && val !== "" && val !== NaN) {
if (this.min_ == null || val < this.min_) {
this.min_ = val;
}
}
};
this.storeResult = function (groupTotals) {
if (!groupTotals.min) {
groupTotals.min = {};
}
groupTotals.min[this.field_] = this.min_;
}
}
function MaxAggregator(field) {
this.field_ = field;
this.init = function () {
this.max_ = null;
};
this.accumulate = function (item) {
var val = item[this.field_];
if (val != null && val !== "" && val !== NaN) {
if (this.max_ == null || val > this.max_) {
this.max_ = val;
}
}
};
this.storeResult = function (groupTotals) {
if (!groupTotals.max) {
groupTotals.max = {};
}
groupTotals.max[this.field_] = this.max_;
}
}
function SumAggregator(field) {
this.field_ = field;
this.init = function () {
this.sum_ = null;
};
this.accumulate = function (item) {
var val = item[this.field_];
if (val != null && val !== "" && val !== NaN) {
this.sum_ += parseFloat(val);
}
};
this.storeResult = function (groupTotals) {
if (!groupTotals.sum) {
groupTotals.sum = {};
}
groupTotals.sum[this.field_] = this.sum_;
}
}
// TODO: add more built-in aggregators
// TODO: merge common aggregators in one to prevent needles iterating
})(jQuery);