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 = $('
').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("", { 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('

' + __('Sort By') + '

\
\
\

' + __('Then By (optional)') + '

\
\

\
'); 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('
' + __("Drag to sort columns") + '
\
\
'); }, 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 = $('
\
\
\
\ \
').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'] = '
{% if has_child_column %}
{% endif %}
{% if can_write %}

{{ __("Tip: Double click cell to edit") }}

{% endif %}
'; 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 = $("
").appendTo(this.page.main); $('\ \
\ ').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 = $("").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) { $('').addClass(dataContext._collapsed ? "expand" : "collapse").css("margin-right", "7px").prependTo($span); } return $span.wrap("

").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(); $("").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 = $('
').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 = $('
').appendTo(this.wrapper); this.page.add_menu_item(__("Export"), function () { return me.export(); }, true); this.grid_wrapper = $("
").appendTo(this.wrapper); this.id = frappe.dom.set_unique_id(this.grid_wrapper.get(0)); $('
\
').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('
%(value)s
', { _style: dataContext._style || "", value: value == null || value === "" ? "" : format_number(value) }); }, text_formatter: function text_formatter(row, cell, value, columnDef, dataContext) { return repl('%(value)s', { _style: dataContext._style || "", esc_value: cstr(value).replace(/"/g, '\"'), value: cstr(value) }); }, check_formatter: function check_formatter(row, cell, value, columnDef, dataContext) { return repl('', { "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('%(value)s', { _style: dataContext._style || "", value: value }); } var link_formatter = me.dataview_columns[cell].link_formatter; if (link_formatter.filter_input) { var html = repl('\ %(value)s', { 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(' \ ', { 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 = ""; 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 + "  " + link; } else { return spacer + "  " + link; } } else { return spacer + "  " + 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('

{0}

\

{1}

\

{2}

\

', [__('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 %}

{{ __(title) }}


{% endif %} {% for col in columns %} {% if col.name && col._id !== "_check" %} {% endif %} {% endfor %} {% for row in data %} {% for col in columns %} {% if col.name && col._id !== "_check" %} {% var value = col.fieldname ? row[col.fieldname] : row[col.id]; %} {% endif %} {% endfor %} {% endfor %}
{{ __(col.name) }}
{{ col.formatter ? col.formatter(row._index, col._index, value, col, row, true) : value }}
'; /*! * 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 = $("
", {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; iThis is pretty much identical to how W3C and jQuery implement events.

* @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. *

Event handler will receive two arguments - an EventData and the data * object the event was fired with.

* @method subscribe * @param fn {Function} Event handler. */ this.subscribe = function (fn) { handlers.push(fn); }; /*** * Removes an event handler added with subscribe(fn). * @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 EventData 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 Event 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 fromRow. * @param toCell {Integer} Optional. Ending cell. Defaults to fromCell. */ 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 = $("

").appendTo($container); $headerScroller = $("
").appendTo($container); $headers = $("
").appendTo($headerScroller); $headers.width(getHeadersWidth()); $headerRowScroller = $("
").appendTo($container); $headerRow = $("
").appendTo($headerRowScroller); $headerRowSpacer = $("
") .css("width", getCanvasWidth() + scrollbarDimensions.width + "px") .appendTo($headerRowScroller); $topPanelScroller = $("
").appendTo($container); $topPanel = $("
").appendTo($topPanelScroller); if (!options.showTopPanel) { $topPanelScroller.hide(); } if (!options.showHeaderRow) { $headerRowScroller.hide(); } $viewport = $("
").appendTo($container); $viewport.css("overflow-y", options.autoHeight ? "hidden" : "auto"); $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 = $("
").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 = $("
").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 = $("
") .html("" + m.name + "") .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(""); } trigger(self.onHeaderCellRendered, { "node": header[0], "column": m }); if (options.showHeaderRow) { var headerRowCell = $("
") .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); $("
") .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 = $("").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 = $("
").appendTo($canvas); el = $("").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 = $("