From d005c207ba61c4892da2ad04182898aac0e9c711 Mon Sep 17 00:00:00 2001 From: Kevin Jhangiani Date: Fri, 25 Mar 2016 02:22:36 +0000 Subject: [PATCH] extracted filters to their own plugin, refactored api logic - filtering plugin is now under datatables-improved-filters - refactored .meta api calls to be more useful and granular - readme updates --- README.md | 22 +- bower.json | 3 +- js/dataTables.metadata.js | 684 ++++++++++++++++++-------------------- 3 files changed, 328 insertions(+), 381 deletions(-) diff --git a/README.md b/README.md index e104b89..bdc4af7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Datatables-Metadata -Datatables Metadata Plugin - Adds API hooks for column metadata, as well as advanced range filters. +Datatables Metadata Plugin - Adds API hooks for column metadata + +This plugin is best used in conjunction with `datatables-improved-filters`, which will allow more advanced column specific filtering. # Installation @@ -31,18 +33,12 @@ This plugin provides the following additional API calls: Store column specific metadata in the datatable. This is used by the filtering functions below, but can also be used to store arbitrary data related to the column as required. This data is state saved if state saving is enabled. - `.column().meta()` // retrieve meta information for a column -- `.column().meta(obj)` // set the metadata for a column to the specified object (replaces any existing metadata) -- `.column().meta(key, data)` // set the `key` property in the column metadata object to `data` - -### Numeric Range Filters - -- `.column().range(min, max)` // numeric range filtering for a column. either min or max can be `null` to enable one sided filtering -- `.column().range()` // clear numeric range filters on this column - -### Date Range Filters (requires moment.js) - -- `.column().dateRange(min, max)` // date range filtering for a column. either min or max can be `null` to enable one sided filtering. Values can be strings (parsed via moment.js) or JS date objects -- `.column().dateRange()` // clear date range filters on this column +- `.column().meta(key)` // retrieve meta information for a column, under the key `key` +- `.column().meta.replace(data)` // set the entire column meta to the object passed in as `data` +- `.column().meta.clear()` // remove all metadata for this column +- `.column().meta.set(key, data)` // set the `key` property in the column metadata object to `data` +- `.column().meta.merge(key, data)` // merge the `key` property in the column metadata object with the object passed in to `data` +- `.column().meta.remove(key)` // remove all metadata under `key` ### Utility diff --git a/bower.json b/bower.json index aebb9c1..b12511f 100644 --- a/bower.json +++ b/bower.json @@ -7,8 +7,7 @@ "description": "jquery-datatables metadata plugin, adds column specific metadata, and useful API calls for range filtering", "main": "js/dataTables.metadata.js", "dependencies": { - "jquery": ">=1.7.0", - "datatables": ">=1.10.8" + }, "keywords": [ "datatables", diff --git a/js/dataTables.metadata.js b/js/dataTables.metadata.js index 8a18a9d..497e65d 100644 --- a/js/dataTables.metadata.js +++ b/js/dataTables.metadata.js @@ -1,96 +1,101 @@ /** - * Metadata + Advanced Filters for Datatables + * Metadata for Datatables * ©2016 Kevin Jhangiani * * Use the 'M' feature flag on the dom: attribute to initialize MetaData plugin * There are no actual dom elements, but the feature flag is used for initialization * + * This plugin simply stores column specific metadata, that persists with state saving. + * The primary use of this plugin is in conjunction with datatables-improved-filters which allow column specific filtering + * * This plugin adds: - * Api.column().meta() // retrieve column specific metadata - * Api.column().meta(@object o) // set column specific metadata to the object parameter (replace) - * Api.column().meta(@string key, @mixed value) // set the column specific metadata for key to value - * - * Api.column().range(min, max) // set a numeric range filter on a column. either element can be null - * Api.column().range() // clear the range filter + * .column().meta() + * .column().meta(key) + * .column().meta.replace(object) + * .column().meta.set(key, value) set key to value + * .column().meta.merge(key, value) merge value into key + * .column().meta.clear() null entire meta + * .column().meta.remove(key) remove key from meta + * + * currently, value should always be an object * - * Api.column().dateRange(min, max) // set a date range filter on a column. either element can be null. requires moment.js - * Api.column().dateRange() // clear the date range filter - * - * Coming soon: Api.column().dateRange(min, max) (requires moment) + * .pick(attr1, attr2, ... attrN) + * .pick([attr1, attr2, ... attrN]) return a new API instance with the specified keys in an object in each index + * similar to pluck, but for multiple arguments * */ // polyfill Array.prototype.fill from: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill if (!Array.prototype.fill) { - Array.prototype.fill = function(value) { - - // Steps 1-2. - if (this == null) { - throw new TypeError('this is null or not defined'); - } - - var O = Object(this); - - // Steps 3-5. - var len = O.length >>> 0; - - // Steps 6-7. - var start = arguments[1]; - var relativeStart = start >> 0; - - // Step 8. - var k = relativeStart < 0 ? - Math.max(len + relativeStart, 0) : - Math.min(relativeStart, len); - - // Steps 9-10. - var end = arguments[2]; - var relativeEnd = end === undefined ? - len : end >> 0; - - // Step 11. - var final = relativeEnd < 0 ? - Math.max(len + relativeEnd, 0) : - Math.min(relativeEnd, len); - - // Step 12. - while (k < final) { - O[k] = value; - k++; - } - - // Step 13. - return O; - }; + Array.prototype.fill = function(value) { + + // Steps 1-2. + if (this == null) { + throw new TypeError('this is null or not defined'); + } + + var O = Object(this); + + // Steps 3-5. + var len = O.length >>> 0; + + // Steps 6-7. + var start = arguments[1]; + var relativeStart = start >> 0; + + // Step 8. + var k = relativeStart < 0 ? + Math.max(len + relativeStart, 0) : + Math.min(relativeStart, len); + + // Steps 9-10. + var end = arguments[2]; + var relativeEnd = end === undefined ? + len : end >> 0; + + // Step 11. + var final = relativeEnd < 0 ? + Math.max(len + relativeEnd, 0) : + Math.min(relativeEnd, len); + + // Step 12. + while (k < final) { + O[k] = value; + k++; + } + + // Step 13. + return O; + }; } (function( factory ){ - if ( typeof define === 'function' && define.amd ) { - // AMD - define( ['jquery', 'datatables.net'], function ( $ ) { - return factory( $, window, document ); - } ); - } - else if ( typeof exports === 'object' ) { - // CommonJS - module.exports = function (root, $) { - if ( ! root ) { - root = window; - } - - if ( ! $ || ! $.fn.dataTable ) { - $ = require('datatables.net')(root, $).$; - } - - return factory( $, root, root.document ); - }; - } - else { - // Browser - factory( jQuery, window, document ); - } + if ( typeof define === 'function' && define.amd ) { + // AMD + define( ['jquery', 'datatables.net'], function ( $ ) { + return factory( $, window, document ); + } ); + } + else if ( typeof exports === 'object' ) { + // CommonJS + module.exports = function (root, $) { + if ( ! root ) { + root = window; + } + + if ( ! $ || ! $.fn.dataTable ) { + $ = require('datatables.net')(root, $).$; + } + + return factory( $, root, root.document ); + }; + } + else { + // Browser + factory( jQuery, window, document ); + } }(function( $, window, document, undefined ) { 'use strict'; var DataTable = $.fn.dataTable; @@ -110,79 +115,79 @@ var _dtMetaData = DataTable.ext.metadata; * @param {[type]} */ var MetaData = function(dt, config) { - this.s = { - dt: new DataTable.Api(dt), - namespace: 'dtmd'+(_instCounter++) - }; + this.s = { + dt: new DataTable.Api(dt), + namespace: 'dtmd'+(_instCounter++) + }; - this._constructor(); + this._constructor(); }; $.extend( MetaData.prototype, { - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Public methods - */ - /** - * Destroy the instance - * elements - */ - destroy: function(){ - //unbind events? - - return this; - }, - - - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * Constructor - */ - - /** - * MetaData constructor - * @private - */ - _constructor: function() { - var that = this; - var dt = this.s.dt; - var dtSettings = dt.settings()[0]; - - // restore saved state if any exists - var loadedState = dt.state.loaded(); - if (loadedState) { loadedState = $.extend(true, {}, {}, loadedState); } - - if (loadedState && loadedState.metadata) { - dtSettings._metadata = loadedState.metadata; - } - - // otherwise, a clean initialization - if (!dtSettings._metadata) { - // dtSettings._metadata = $.extend(true, {}, this.s.metadata); - - dtSettings._metadata = {}; - - // apparently, doing .fill with {} as the parameter makes the entire array reference the same object - // changed to null to alleviate this - dtSettings._metadata.columns = new Array(dtSettings.aoColumns.length).fill(null); - } - - // plugin is enabled (for short-circuiting search) - dtSettings._metadataEnabled = true; - - // bind events - - // on destroy, cleanup - dt.on('destroy.dt', function () { - that.destroy(); - }); - - // on save, add the metadata to the state - dt.on('stateSaveParams.dt', function(e, settings, data) { - if (settings._metadataEnabled) { - data.metadata = $.extend(true, {}, {}, settings._metadata); //deep copy? - } - }); - } + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Public methods + */ + /** + * Destroy the instance + * elements + */ + destroy: function(){ + //unbind events? + + return this; + }, + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Constructor + */ + + /** + * MetaData constructor + * @private + */ + _constructor: function() { + var that = this; + var dt = this.s.dt; + var dtSettings = dt.settings()[0]; + + // restore saved state if any exists + var loadedState = dt.state.loaded(); + if (loadedState) { loadedState = $.extend(true, {}, {}, loadedState); } + + if (loadedState && loadedState.metadata) { + dtSettings._metadata = loadedState.metadata; + } + + // otherwise, a clean initialization + if (!dtSettings._metadata) { + // dtSettings._metadata = $.extend(true, {}, this.s.metadata); + + dtSettings._metadata = {}; + + // apparently, doing .fill with {} as the parameter makes the entire array reference the same object + // changed to null to alleviate this + dtSettings._metadata.columns = new Array(dtSettings.aoColumns.length).fill(null); + } + + // plugin is enabled (for short-circuiting search) + dtSettings._metadataEnabled = true; + + // bind events + + // on destroy, cleanup + dt.on('destroy.dt', function () { + that.destroy(); + }); + + // on save, add the metadata to the state + dt.on('stateSaveParams.dt', function(e, settings, data) { + if (settings._metadataEnabled) { + data.metadata = $.extend(true, {}, {}, settings._metadata); //deep copy? + } + }); + } }); @@ -199,7 +204,55 @@ $.extend( MetaData.prototype, { * @static */ MetaData.defaults = { - columns: [], + columns: [], +}; + +MetaData._initData = function(metaObj, index) { + if (!metaObj[index]) { metaObj[index] = {}; } + + return metaObj[index]; +}; + +MetaData._getData = function(metaObj, index) { + if (!metaObj || !metaObj[index]) { return null; } + return $.extend(true, {}, {}, metaObj[index]); +}; + +MetaData._getKeyData = function(metaObj, index, key) { + var meta = MetaData._getData(metaObj, index); + if (meta && meta.hasOwnProperty(key)) { + return $.extend(true, {}, {}, meta[key]); + } + return null; +}; + +MetaData._clearData = function(metaObj, index) { + var meta = MetaData._initData(metaObj, index); + meta = null; +}; + +MetaData._replaceData = function(metaObj, index, data) { + var meta = MetaData._initData(metaObj, index); + meta = $.extend(true, {}, {}, data); +}; + +MetaData._setKeyData = function(metaObj, index, key, data) { + var meta = MetaData._initData(metaObj, index); + meta[key] = $.extend(true, {}, {}, data); +}; + +MetaData._mergeKeyData = function(metaObj, index, key, data) { + var meta = MetaData._initData(metaObj, index); + if (!meta.hasOwnProperty(key)) { meta[key] = {}; } + + meta[key] = $.extend(true, {}, meta[key], data); +}; + +MetaData._removeKeyData = function(metaObj, index, key) { + var meta = MetaData._initData(metaObj, index); + if (meta && meta.hasOwnProperty(key)) { + delete meta[key]; + } }; /** @@ -217,234 +270,133 @@ MetaData.version = '0.3.0'; /** - .column().meta() - .column().meta(object) - .column().meta(key, value) set key to value - - utility fn to retrieve or set column specific state - this function modifies the settings.columns object of the datatable - - if used as a setter, returns this for chaining - if used as a getter, returns the column state, or null - - @todo: not tested against multi table api calls + .column().meta() + .column().meta(key) + .column().meta.replace(object) + .column().meta.set(key, value) set key to value + .column().meta.merge(key, value) merge value into key + .column().meta.clear() null entire meta + .column().meta.remove(key) + + @todo: not tested against multi table api calls **/ DataTable.Api.register( 'column().meta()', function() { - var metadata = this.settings()[0]._metadata; - - // we have not loaded the plugin, either return null or this - // return value depends on arguments.length, whether this was called as a setter or getter - if (!metadata) { - return (!arguments.length ? null : this); - } - - var colIndex = this.index(); - var args = Array.prototype.slice.call(arguments); - - //getter - if (!args.length) { - if (metadata && metadata.columns && metadata.columns[colIndex]){ - return $.extend(true, {}, metadata.columns[colIndex]); - } - return null; - } - - //setter - else { - if (metadata && !metadata.columns) { metadata.columns = []; } - if (metadata && metadata.columns && !metadata.columns[colIndex]) { metadata.columns[colIndex] = {}; } - if (metadata && metadata.columns && metadata.columns[colIndex]){ - // set entire metadata object - if (args.length === 1) { - // metadata.columns[colIndex] = $.extend(true, {}, {}, args[0]); - if (typeof args[0] === 'object') { - metadata.columns[colIndex] = args[0]; - } - else { - metadata.columns[colIndex] = {}; - } - } - // set metadata key to value - else { - var metaKey = args[0]; - var metaValue = args[1]; - metadata.columns[colIndex][metaKey] = metaValue; - } - - // save state after saving metadata - this.state.save(); - } - - return this; - } - + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, return null + if (!metadata) { + return null; + } + + var colIndex = this.index(); + var args = Array.prototype.slice.call(arguments); + + //getter + if (!args.length) { + return MetaData._getData(metadata.columns, colIndex); + } + else { + return MetaData._getKeyData(metadata.columns, colIndex, args[0]); + } }); - -/** - .pick(arg1, arg2, arg3...argN) - - return a new Api instance with the arguments passed in as the keys - similar to pluck, but accepts multiple keys to extract, and returns objects -**/ -DataTable.Api.register( 'pick()', function() { - var item; - var args = Array.prototype.slice.call(arguments); - - // support [] as first parameter - if (args.length === 1 && $.isArray(args[0])) { - args = args[0]; - } - - return this.map(function(value, i) { - item = {}; - args.forEach(function(prop) { - item[prop] = value[prop]; - }); - - return item; - }); +DataTable.Api.register( 'column().meta.replace()', function(replaceData) { + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, either return null or this + // return value depends on arguments.length, whether this was called as a setter or getter + if (!metadata) { + return this; + } + + var colIndex = this.index(); + MetaData._replaceData(metadata.columns, colIndex, replaceData); + + return this; }); +DataTable.Api.register( 'column().meta.set()', function(key, data) { + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, return this to chain + if (!metadata) { + return this; + } + + var colIndex = this.index(); + MetaData._setKeyData(metadata.columns, colIndex, key, data); + + return this; +}); -/** - .column().range() - .column().range(min, max) - - sets or clears column specific (numeric) range filter - utilizes the registered search fn, and depends on the key settings.columns[i].range - - uses .column().meta() to store column specific data - - if metadata is not initialized, will not do anything - - chainable, returns Api -**/ -DataTable.Api.register('column().range()', function (lowerBound, upperBound) { - var colIndex = this.index(); - - // metadata plugin not loaded, just return - if (this.settings()[0]._metadataEnabled !== true) { return this; } - - // if no parameters, or only a single null parameter, then clear the search state - if (!arguments.length || (arguments.length === 1 && lowerBound === null)) { - this.column(colIndex).meta('range', null); - return this; - } - - // if we have parameters, set them - // @todo: validate these values - this.column(colIndex).meta('range', { min: lowerBound, max: upperBound }); - return this; +DataTable.Api.register( 'column().meta.merge()', function(key, data) { + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, return this to chain + if (!metadata) { + return this; + } + + var colIndex = this.index(); + MetaData._mergeKeyData(metadata.columns, colIndex, key, data); + + return this; }); -/** - .column().dateRange() - .column().dateRange(min, max) - - sets or clears column specific (date) range filter - utilizes the registered search fn, and depends on the metadata key dateRange - - uses .column().meta() to store column specific data - - if metadata is not initialized, will not do anything - - chainable, returns Api -**/ -DataTable.Api.register('column().dateRange()', function (lowerBound, upperBound) { - var colIndex = this.index(); - - // metadata plugin not loaded, just return - if (this.settings()[0]._metadataEnabled !== true) { return this; } - - // if no parameters, or only a single null parameter, then clear the search state - if (!arguments.length || (arguments.length === 1 && lowerBound === null)) { - this.column(colIndex).meta('dateRange', null); - return this; - } - - // if we have parameters, set them - // @todo: validate these values - this.column(colIndex).meta('dateRange', { min: lowerBound, max: upperBound }); - return this; +DataTable.Api.register( 'column().meta.remove()', function(key) { + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, return this to chain + if (!metadata) { + return this; + } + + var colIndex = this.index(); + MetaData._removeKeyData(metadata.columns, colIndex, key); + + return this; }); +DataTable.Api.register( 'column().meta.clear()', function() { + var metadata = this.settings()[0]._metadata; + + // we have not loaded the plugin, return this to chain + if (!metadata) { + return this; + } + + var colIndex = this.index(); + MetaData._clearData(metadata.columns, colIndex); + + return this; +}); -/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - * DataTables Search Hooks - * - */ /** - custom .ext.search for numeric and date range filtering - uses settings.columns[i].range to compute search + .pick(arg1, arg2, arg3...argN) + + return a new Api instance with the arguments passed in as the keys + similar to pluck, but accepts multiple keys to extract, and returns objects **/ -DataTable.ext.search.push(function(settings, searchData, index, rowData, counter) { - // if metadata is not enabled, skip this search - if (settings._metadataEnabled !== true) { return true; } - - // if we have no metadata, or no columns, then short-circuit true - var metadata = settings._metadata; - if (!metadata || !metadata.columns || !metadata.columns.length) { return true; } - - var isBoundValid = function(bound) { - // false, null, and isNaN all exclude the row - if (bound === null || bound === false || isNaN(bound)) { return false; } - return true; - }; - - var checkRange = function(colData, min, max) { - if (!isBoundValid(colData)) { return false; } - - if ((!isBoundValid(min) && !isBoundValid(max)) || - (!isBoundValid(min) && colData <= max) || - (min <= colData && !isBoundValid(max)) || - (min <= colData && colData <= max )) { - - return true; - } - return false; - }; - var range; - var dateRange; - var dateMin; - var dateMax; - - for (var i=0,ien=metadata.columns.length; i < ien; i++) { - if (!metadata.columns[i]) { continue; } //skip to next iteration if no metadata on this column - - range = metadata.columns[i].range; - - if (range && (range.hasOwnProperty('min') || range.hasOwnProperty('max'))) { - if (!checkRange(searchData[i], range.min, range.max)) { - // if we fail the check, remove this row - return false; - } - // otherwise, continue checking rows - } - - - dateRange = metadata.columns[i].dateRange; - if (moment && dateRange && (dateRange.hasOwnProperty('min') || dateRange.hasOwnProperty('max'))) { - if (dateRange.hasOwnProperty('min')) { dateMin = moment(dateRange.min).format('x'); } - else { dateMin = null; } - - if (dateRange.hasOwnProperty('max')) { dateMax = moment(dateRange.max).format('x'); } - else { dateMax = null; } - - if (!checkRange(moment(searchData[i]).format('x'), dateMin, dateMax)) { - // if we fail the date range check, remove this row - return false; - } - // otherwise, continue checking rows - } - } - - // if we got here, we are good - return true; +DataTable.Api.register( 'pick()', function() { + var item; + var args = Array.prototype.slice.call(arguments); + + // support [] as first parameter + if (args.length === 1 && $.isArray(args[0])) { + args = args[0]; + } + + return this.map(function(value, i) { + item = {}; + args.forEach(function(prop) { + item[prop] = value[prop]; + }); + + return item; + }); }); - /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * DataTables interface */ @@ -455,17 +407,17 @@ $.fn.DataTable.MetaData = MetaData; // DataTables `dom` feature option DataTable.ext.feature.push({ - fnInit: function(settings) { - var api = new DataTable.Api( settings ); - var opts = {}; - - var adv = new MetaData( api, opts ); - return null; - }, - - // enable with 'M' feature flag - cFeature: "M" - + fnInit: function(settings) { + var api = new DataTable.Api( settings ); + var opts = {}; + + var adv = new MetaData( api, opts ); + return null; + }, + + // enable with 'M' feature flag + cFeature: "M" + });