/*** * Grid.Mvc * Examples and documentation at: http://gridmvc.codeplex.com * Version: 3.0.0 * Requires: window.jQuery v1.3+ * LGPL license: http://gridmvc.codeplex.com/license */ window.pageGrids = window.pageGrids || {}; $.fn.extend({ gridmvc: function () { var aObj = []; $(this).each(function () { if (!$(this).data("gridmvc")) { var options = { lang: $(this).attr("data-lang"), selectable: $(this).attr("data-selectable") == "true", multiplefilters: $(this).attr("data-multiplefilters") == "true" }; var grid = new GridMvc(this, options); var name = $(this).attr("data-gridname"); if (name.length > 0) window.pageGrids[$(this).attr("data-gridname")] = grid; aObj.push(grid); $(this).data("gridmvc", grid); } else { aObj.push($(this).data("gridmvc")); } }); if (aObj.length == 1) return aObj[0]; return aObj; } }); GridMvc = (function ($) { function gridMvc(container, options) { this.jqContainer = $(container); options = options || {}; this.options = $.extend({}, this.defaults(), options); this.init(); } gridMvc.prototype.init = function () { //load current lang options: this.lang = GridMvc.lang[this.options.lang]; if (typeof (this.lang) == 'undefined') this.lang = GridMvc.lang.en; this.events = []; if (this.options.selectable) this.initGridRowsEvents(); //Register default filter widgets: this.filterWidgets = []; this.addFilterWidget(new TextFilterWidget()); this.addFilterWidget(new NumberFilterWidget()); this.addFilterWidget(new DateTimeFilterWidget()); this.addFilterWidget(new BooleanFilterWidget()); this.openedMenuBtn = null; this.initFilters(); }; /*** * Handle Grid row events */ gridMvc.prototype.initGridRowsEvents = function () { var $this = this; this.jqContainer.on("click", ".grid-row", function () { $this.rowClicked.call(this, $this); }); }; /*** * Trigger on Grid row click */ gridMvc.prototype.rowClicked = function ($context) { if (!$context.options.selectable) return; var row = $(this).closest(".grid-row"); if (row.length <= 0) return; var gridRow = {}; row.find(".grid-cell").each(function () { var columnName = $(this).attr("data-name"); if (columnName.length > 0) gridRow[columnName] = $(this).text(); }); var evt = $.Event("RowClicked"); $context.notifyOnRowSelect(gridRow, evt); if (!evt.isDefaultPrevented()) $context.markRowSelected(row); }; /*** * Mark Grid row as selected */ gridMvc.prototype.markRowSelected = function (row) { this.jqContainer.find(".grid-row.grid-row-selected").removeClass("grid-row-selected"); row.addClass("grid-row-selected"); }; /*** * Default Grid.Mvc options */ gridMvc.prototype.defaults = function () { return { selectable: true, multiplefilters: false, lang: 'en' }; }; /*** * ============= EVENTS ============= * Methods that provides functionality for grid events */ gridMvc.prototype.onRowSelect = function (func) { this.events.push({ name: "onRowSelect", callback: func }); }; gridMvc.prototype.notifyOnRowSelect = function (row, e) { e.row = row; this.notifyEvent("onRowSelect", e); }; gridMvc.prototype.notifyEvent = function (eventName, e) { for (var i = 0; i < this.events.length; i++) { if (this.events[i].name == eventName) if (!this.events[i].callback(e)) break; } }; /*** * ============= FILTERS ============= * Methods that provides functionality for filtering */ /*** * Method search all filter buttons and register 'onclick' event handlers: */ gridMvc.prototype.initFilters = function () { var filterHtml = this.filterMenuHtml(); var self = this; this.jqContainer.find(".grid-filter").each(function () { $(this).click(function () { return self.openFilterPopup.call(this, self, filterHtml); }); }); }; /*** * Shows filter popup window and render filter widget */ gridMvc.prototype.openFilterPopup = function (self, html) { //retrive all column filter parameters from html attrs: var columnType = $(this).attr("data-type") || ""; //determine widget var widget = self.getFilterWidgetForType(columnType); //if widget for specified column type not found - do nothing if (widget == null) return false; //if widget allready rendered - just open popup menu: if (this.hasAttribute("data-rendered")) { var or = self.openMenuOnClick.call(this, self); self.setupPopupInitialPosition($(this)); if (!or && typeof (widget.onShow) != 'undefined') widget.onShow(); return or; } var columnName = $(this).attr("data-name") || ""; var filterData = $(this).attr("data-filterdata") || ""; var widgetData = $(this).attr("data-widgetdata") || "{}"; var filterDataObj = self.parseFilterValues(filterData) || {}; var filterUrl = $(this).attr("data-url") || ""; //mark filter as rendered $(this).attr("data-rendered", "1"); //append base popup layout: $(this).append(html); //determine widget container: var widgetContainer = $(this).find(".grid-popup-widget"); //onRender target widget if (typeof (widget.onRender) != 'undefined') widget.onRender(widgetContainer, self.lang, columnType, filterDataObj, function (values) { self.closeOpenedPopups(); self.applyFilterValues(filterUrl, columnName, values, false); }, $.parseJSON(widgetData)); //adding 'clear filter' button if needed: if ($(this).find(".grid-filter-btn").hasClass("filtered") && widget.showClearFilterButton()) { var inner = $(this).find(".grid-popup-additional"); inner.append(self.getClearFilterButton(filterUrl)); inner.find(".grid-filter-clear").click(function () { self.applyFilterValues(filterUrl, columnName, "", true); }); } var openResult = self.openMenuOnClick.call(this, self); if (typeof (widget.onShow) != 'undefined') widget.onShow(); self.setupPopupInitialPosition($(this)); return openResult; }; gridMvc.prototype.setupPopupInitialPosition = function (popup) { var drop = popup.find(".grid-dropdown"); function getInfo() { var arrow = popup.find(".grid-dropdown-arrow"); return { arrow: arrow, currentDropLeft: parseInt(drop.css("left")), currentArrowLeft: parseInt(arrow.css("left")) }; } var dropLeft = drop.offset().left; if (dropLeft < 0) { var info = getInfo(); info.arrow.css({ left: (info.currentArrowLeft + dropLeft - 10) + "px" }); drop.css({ left: (info.currentDropLeft - dropLeft + 10) + "px" }); return; } var dropWidth = drop.width(); var offsetRight = $(window).width() - (dropLeft + dropWidth); if (offsetRight < 0) { var info = getInfo(); info.arrow.css({ left: (info.currentArrowLeft - offsetRight + 10) + "px" }); drop.css({ left: (info.currentDropLeft + offsetRight - 10) + "px" }); } }; /*** * Returns layout of filter popup menu */ gridMvc.prototype.filterMenuHtml = function () { return ''; }; /*** * Returns layout of 'clear filter' button */ gridMvc.prototype.getClearFilterButton = function () { return ''; }; /*** * Register filter widget in widget collection */ gridMvc.prototype.addFilterWidget = function (widget) { this.filterWidgets.push(widget); }; /*** * Parse filter settings from data attribute */ gridMvc.prototype.parseFilterValues = function (filterData) { var opt = $.parseJSON(filterData); var filters = []; for (var i = 0; i < opt.length; i++) { filters.push({ filterValue: this.urldecode(opt[i].FilterValue), filterType: opt[i].FilterType, columnName: opt[i].ColumnName }); } return filters; }; gridMvc.prototype.urldecode = function (str) { return decodeURIComponent((str + '').replace(/\+/g, '%20')); }; /*** * Return registred widget for specific column type name */ gridMvc.prototype.getFilterWidgetForType = function (typeName) { for (var i = 0; i < this.filterWidgets.length; i++) { if ($.inArray(typeName, this.filterWidgets[i].getAssociatedTypes()) >= 0) return this.filterWidgets[i]; } return null; }; /*** * Replace existed filter widget */ gridMvc.prototype.replaceFilterWidget = function (typeNameToReplace, widget) { for (var i = 0; i < this.filterWidgets.length; i++) { if ($.inArray(typeNameToReplace, this.filterWidgets[i].getAssociatedTypes()) >= 0) { this.filterWidgets.splice(i, 1); this.addFilterWidget(widget); return true; } } return false; }; /*** * Applies selected filter value by redirecting to another url: */ gridMvc.prototype.applyFilterValues = function (initialUrl, columnName, values, skip) { var filters = this.jqContainer.find(".grid-filter"); if (initialUrl.length > 0) initialUrl += "&"; var url = ""; if (!skip) { url += this.getFilterQueryData(columnName, values); } if (this.options.multiplefilters) { //multiple filters enabled for (var i = 0; i < filters.length; i++) { if ($(filters[i]).attr("data-name") != columnName) { var filterData = this.parseFilterValues($(filters[i]).attr("data-filterdata")); if (filterData.length == 0) continue; if (url.length > 0) url += "&"; url += this.getFilterQueryData($(filters[i]).attr("data-name"), filterData); } else { continue; } } } window.location.search = initialUrl + url; }; gridMvc.prototype.getFilterQueryData = function (columnName, values) { var url = ""; for (var i = 0; i < values.length; i++) { url += "grid-filter=" + encodeURIComponent(columnName) + "__" + values[i].filterType + "__" + encodeURIComponent(values[i].filterValue); if (i != values.length - 1) url += "&"; } return url; }; /*** * ============= POPUP MENU ============= * Methods that provides base functionality for popup menus */ gridMvc.prototype.openMenuOnClick = function (self) { if ($(this).hasClass("clicked")) return true; self.closeOpenedPopups(); $(this).addClass("clicked"); var popup = $(this).find(".dropdown-menu"); if (popup.length == 0) return true; popup.show(); popup.addClass("opened"); self.openedMenuBtn = $(this); $(document).bind("click.gridmvc", function (e) { self.documentCallback(e, self); }); return false; }; gridMvc.prototype.documentCallback = function (e, $context) { e = e || event; var target = e.target || e.srcElement; var box = $(".dropdown-menu.opened").get(0); var html = $("html").get(0); if (typeof box != "undefined") { do { if (box == target) { // Click occured inside the box, do nothing. return; } if (html == target) { box.style.display = "none"; $(box).removeClass("opened"); } target = target.parentNode; } while (target); // Click was outside the box, hide it. } if ($context.openedMenuBtn != null) $context.openedMenuBtn.removeClass("clicked"); $(document).unbind("click.gridmvc"); }; gridMvc.prototype.closeOpenedPopups = function () { var openedPopup = $(".dropdown-menu.opened"); openedPopup.hide(); openedPopup.removeClass("opened"); if (this.openedMenuBtn != null) this.openedMenuBtn.removeClass("clicked"); }; /* Grid.Mvc clients functions */ gridMvc.prototype.selectable = function (enable) { this.options.selectable = enable; }; return gridMvc; })(window.jQuery); /*** * ============= LOCALIZATION ============= * You can localize Grid.Mvc by creating localization files and include it on the page after this script file * This script file provides localization only for english language. * For more documentation see: http://gridmvc.codeplex.com/documentation */ if (typeof (GridMvc.lang) == 'undefined') GridMvc.lang = {}; GridMvc.lang.en = { filterTypeLabel: "Type: ", filterValueLabel: "Value:", applyFilterButtonText: "Apply", filterSelectTypes: { Equals: "Equals", StartsWith: "StartsWith", Contains: "Contains", EndsWith: "EndsWith", GreaterThan: "Greater than", LessThan: "Less than" }, code: 'en', boolTrueLabel: "Yes", boolFalseLabel: "No", clearFilterLabel: "Clear filter" }; /*** * ============= FILTER WIDGETS ============= * Filter widget allows onRender custom filter user interface for different columns. * For example if your added column is of type "DateTime" - widget can onRender calendar for picking filter value. * This script provider base widget for default .Net types: System.String, System.Int32, System.Decimal etc. * If you want to provide custom filter functionality - you can assign your own widget type for column and write widget for this types. * For more documentation see: http://gridmvc.codeplex.com/documentation */ /*** * TextFilterWidget - Provides filter interface for text columns (of type "System.String") * This widget onRenders filter interface with conditions, which specific for text types: contains, starts with etc. */ TextFilterWidget = (function ($) { function textFilterWidget() { } /*** * This method must return type of columns that must be associated with current widget * If you not specify your own type name for column (see 'SetFilterWidgetType' method), GridMvc setup column type name from .Net type ("System.DateTime etc.) */ textFilterWidget.prototype.getAssociatedTypes = function () { return ["System.String"]; }; /*** * This method invokes when filter widget was shown on the page */ textFilterWidget.prototype.onShow = function () { var textBox = this.container.find(".grid-filter-input"); if (textBox.length <= 0) return; textBox.focus(); }; /*** * This method specify whether onRender 'Clear filter' button for this widget. */ textFilterWidget.prototype.showClearFilterButton = function () { return true; }; /*** * This method will invoke when user first clicked on filter button. * container - html element, which must contain widget layout; * lang - current language settings; * typeName - current column type (if widget assign to multipile types, see: getAssociatedTypes); * filterType - current filter type (like equals, starts with etc); * filterValue - current filter value; * cb - callback function that must invoked when user want to filter this column. Widget must pass filter type and filter value. */ textFilterWidget.prototype.onRender = function (container, lang, typeName, values, cb) { this.cb = cb; this.container = container; this.lang = lang; this.value = values.length > 0 ? values[0] : { filterType: 1, filterValue: "" };//support only one filter value this.renderWidget(); this.registerEvents(); }; /*** * Internal method that build widget layout and append it to the widget container */ textFilterWidget.prototype.renderWidget = function () { var html = '
\ \ \
\
\ \ \
\
\ \
'; this.container.append(html); }; /*** * Internal method that register event handlers for 'apply' button. */ textFilterWidget.prototype.registerEvents = function () { //get apply button from: var applyBtn = this.container.find(".grid-apply"); //save current context: var $context = this; //register onclick event handler applyBtn.click(function () { //get selected filter type: var type = $context.container.find(".grid-filter-type").val(); //get filter value: var value = $context.container.find(".grid-filter-input").val(); //invoke callback with selected filter values: var filterValues = [{ filterType: type, filterValue: value }]; $context.cb(filterValues); }); //register onEnter event for filter text box: this.container.find(".grid-filter-input").keyup(function (event) { if (event.keyCode == 13) { applyBtn.click(); } if (event.keyCode == 27) { GridMvc.closeOpenedPopups(); } }); }; return textFilterWidget; })(window.jQuery); /*** * NumberFilterWidget - Provides filter interface for number columns * This widget onRenders filter interface with conditions, which specific for number types: great than, less that etc. * Also validates user's input for digits */ NumberFilterWidget = (function ($) { function numberFilterWidget() { } numberFilterWidget.prototype.showClearFilterButton = function () { return true; }; numberFilterWidget.prototype.getAssociatedTypes = function () { return ["System.Int32", "System.Double", "System.Decimal", "System.Byte", "System.Single", "System.Float", "System.Int64"]; }; numberFilterWidget.prototype.onShow = function () { var textBox = this.container.find(".grid-filter-input"); if (textBox.length <= 0) return; textBox.focus(); }; numberFilterWidget.prototype.onRender = function (container, lang, typeName, values, cb) { this.cb = cb; this.container = container; this.lang = lang; this.typeName = typeName; this.value = values.length > 0 ? values[0] : { filterType: 1, filterValue: "" };//support only one filter value this.renderWidget(); this.registerEvents(); }; numberFilterWidget.prototype.renderWidget = function () { var html = '
\ \ \
\
\ \ \
\
\ \
'; this.container.append(html); }; numberFilterWidget.prototype.registerEvents = function () { var $context = this; var applyBtn = this.container.find(".grid-apply"); applyBtn.click(function () { var type = $context.container.find(".grid-filter-type").val(); var value = $context.container.find(".grid-filter-input").val(); var filters = [{ filterType: type, filterValue: value }]; $context.cb(filters); }); var txt = this.container.find(".grid-filter-input"); txt.keyup(function (event) { if (event.keyCode == 13) { applyBtn.click(); } if (event.keyCode == 27) { GridMvc.closeOpenedPopups(); } }) .keypress(function (event) { return $context.validateInput.call($context, event); }); if (this.typeName == "System.Byte") txt.attr("maxlength", "3"); }; numberFilterWidget.prototype.validateInput = function (evt) { var $event = evt || window.event; var key = $event.keyCode || $event.which; key = String.fromCharCode(key); var regex; switch (this.typeName) { case "System.Byte": case "System.Int32": case "System.Int64": regex = /[0-9]/; break; default: regex = /[0-9]|\.|\,/; } if (!regex.test(key)) { $event.returnValue = false; if ($event.preventDefault) $event.preventDefault(); } }; return numberFilterWidget; })(window.jQuery); /*** * DateTimeFilterWidget - Provides filter interface for date columns (of type "System.DateTime"). * If datepicker script included, this widget will render calendar for pick filter values * In other case he onRender textbox field for specifing date value (more info at http://window.jQueryui.com/) */ DateTimeFilterWidget = (function ($) { function dateTimeFilterWidget() { } dateTimeFilterWidget.prototype.getAssociatedTypes = function () { return ["System.DateTime"]; }; dateTimeFilterWidget.prototype.showClearFilterButton = function () { return true; }; dateTimeFilterWidget.prototype.onRender = function (container, lang, typeName, values, applycb, data) { this.datePickerIncluded = typeof ($.fn.datepicker) != 'undefined'; this.cb = applycb; this.data = data; this.container = container; this.lang = lang; this.value = values.length > 0 ? values[0] : { filterType: 1, filterValue: "" };//support only one filter value this.renderWidget(); this.registerEvents(); }; dateTimeFilterWidget.prototype.renderWidget = function () { var html = '
\ \ \
' + (this.datePickerIncluded ? '
' : '
\ \ \
\
\ \
'); this.container.append(html); //if window.jQueryUi included: if (this.datePickerIncluded) { var datePickerOptions = this.data || {}; datePickerOptions.format = datePickerOptions.format || "yyyy-mm-dd"; datePickerOptions.language = datePickerOptions.language || this.lang.code; var $context = this; var dateContainer = this.container.find(".grid-filter-datepicker"); dateContainer.datepicker(datePickerOptions).on('changeDate', function (ev) { var type = $context.container.find(".grid-filter-type").val(); var filterValues = [{ filterType: type, filterValue: ev.format() }]; $context.cb(filterValues); }); if (this.value.filterValue) dateContainer.datepicker('update', this.value.filterValue); } }; dateTimeFilterWidget.prototype.registerEvents = function () { var $context = this; var applyBtn = this.container.find(".grid-apply"); applyBtn.click(function () { var type = $context.container.find(".grid-filter-type").val(); var value = $context.container.find(".grid-filter-input").val(); var filterValues = [{ filterType: type, filterValue: value }]; $context.cb(filterValues); }); this.container.find(".grid-filter-input").keyup(function (event) { if (event.keyCode == 13) { applyBtn.click(); } }); }; return dateTimeFilterWidget; })(window.jQuery); /*** * BooleanFilterWidget - Provides filter interface for boolean columns (of type "System.Boolean"). * Renders two button for filter - true and false */ BooleanFilterWidget = (function ($) { function booleanFilterWidget() { } booleanFilterWidget.prototype.getAssociatedTypes = function () { return ["System.Boolean"]; }; booleanFilterWidget.prototype.showClearFilterButton = function () { return true; }; booleanFilterWidget.prototype.onRender = function (container, lang, typeName, values, cb) { this.cb = cb; this.container = container; this.lang = lang; this.value = values.length > 0 ? values[0] : { filterType: 1, filterValue: "" };//support only one filter value this.renderWidget(); this.registerEvents(); }; booleanFilterWidget.prototype.renderWidget = function () { var html = '\ '; this.container.append(html); }; booleanFilterWidget.prototype.registerEvents = function () { var $context = this; var applyBtn = this.container.find(".grid-filter-choose"); applyBtn.click(function () { var filterValues = [{ filterType: "1", filterValue: $(this).attr("data-value") }]; $context.cb(filterValues); }); }; return booleanFilterWidget; })(window.jQuery); //startup init: (function ($) { if (!$) return;//jquery not referenced $(function () { $(".grid-mvc").each(function () { $(".grid-mvc").gridmvc(); }); }); })(window.jQuery);