diff --git a/geonode/static/geosafe/css/jquery.dynatable.css b/geonode/static/geosafe/css/jquery.dynatable.css
new file mode 100755
index 00000000000..6ea0b6472d3
--- /dev/null
+++ b/geonode/static/geosafe/css/jquery.dynatable.css
@@ -0,0 +1,62 @@
+/*
+ * jQuery Dynatable plugin 0.3.1
+ *
+ * Copyright (c) 2014 Steve Schwartz (JangoSteve)
+ *
+ * Dual licensed under the AGPL and Proprietary licenses:
+ * http://www.dynatable.com/license/
+ *
+ * Date: Tue Jan 02 2014
+ */
+
+.dynatable-search {
+ float: right;
+ margin-bottom: 10px;
+}
+
+.dynatable-pagination-links {
+ float: right;
+}
+
+.dynatable-record-count {
+ display: block;
+ padding: 5px 0;
+}
+
+.dynatable-pagination-links span,
+.dynatable-pagination-links li {
+ display: inline-block;
+}
+
+.dynatable-page-link,
+.dynatable-page-break {
+ display: block;
+ padding: 5px 7px;
+}
+
+.dynatable-page-link {
+ cursor: pointer;
+}
+
+.dynatable-active-page,
+.dynatable-disabled-page {
+ cursor: text;
+}
+.dynatable-active-page:hover,
+.dynatable-disabled-page:hover {
+ text-decoration: none;
+}
+
+.dynatable-active-page {
+ background: #71AF5A;
+ border-radius: 5px;
+ color: #fff;
+}
+.dynatable-active-page:hover {
+ color: #fff;
+}
+.dynatable-disabled-page,
+.dynatable-disabled-page:hover {
+ background: none;
+ color: #999;
+}
diff --git a/geonode/static/geosafe/css/main.css b/geonode/static/geosafe/css/main.css
new file mode 100644
index 00000000000..eb73c04a708
--- /dev/null
+++ b/geonode/static/geosafe/css/main.css
@@ -0,0 +1,49 @@
+/*
+On Off Switch
+*/
+.onoffswitch {
+ position: relative; width: 90px;
+ -webkit-user-select:none; -moz-user-select:none; -ms-user-select: none;
+}
+.onoffswitch-checkbox {
+ display: none;
+}
+.onoffswitch-label {
+ display: block; overflow: hidden; cursor: pointer;
+ border: 2px solid #999999; border-radius: 20px;
+ margin-right: 0px;
+}
+.onoffswitch-inner {
+ display: block; width: 200%; margin-left: -100%;
+ transition: margin 0.3s ease-in 0s;
+}
+.onoffswitch-inner:before, .onoffswitch-inner:after {
+ display: block; float: left; width: 50%; height: 30px; padding: 0; line-height: 30px;
+ font-size: 14px; color: white; font-family: Trebuchet, Arial, sans-serif; font-weight: bold;
+ box-sizing: border-box;
+}
+.onoffswitch-inner:before {
+ content: "ON";
+ padding-left: 10px;
+ background-color: #50cf5c; color: #FFFFFF;
+}
+.onoffswitch-inner:after {
+ content: "OFF";
+ padding-right: 10px;
+ background-color: #EEEEEE; color: #999999;
+ text-align: right;
+}
+.onoffswitch-switch {
+ display: block; width: 18px; margin: 8px; height: 18px;
+ background: #FFFFFF;
+ position: absolute; top: 0; bottom: 0;
+ right: 56px;
+ border: 2px solid #999999; border-radius: 20px;
+ transition: all 0.3s ease-in 0s;
+}
+.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-inner {
+ margin-left: 0;
+}
+.onoffswitch-checkbox:checked + .onoffswitch-label .onoffswitch-switch {
+ right: 0px;
+}
diff --git a/geonode/static/geosafe/img/impact.svg b/geonode/static/geosafe/img/impact.svg
old mode 100755
new mode 100644
index 9c176690269..b86db08cc7f
--- a/geonode/static/geosafe/img/impact.svg
+++ b/geonode/static/geosafe/img/impact.svg
@@ -2,9 +2,7 @@
diff --git a/geonode/static/geosafe/img/impact_function.svg b/geonode/static/geosafe/img/impact_function.svg
new file mode 100644
index 00000000000..433b4a16147
--- /dev/null
+++ b/geonode/static/geosafe/img/impact_function.svg
@@ -0,0 +1,21 @@
+
+
+
+
diff --git a/geonode/static/geosafe/img/inasafe-icon.png b/geonode/static/geosafe/img/inasafe-icon.png
new file mode 100644
index 00000000000..1688845ba05
Binary files /dev/null and b/geonode/static/geosafe/img/inasafe-icon.png differ
diff --git a/geonode/static/geosafe/js/jquery.dynatable.js b/geonode/static/geosafe/js/jquery.dynatable.js
new file mode 100755
index 00000000000..e30332fb659
--- /dev/null
+++ b/geonode/static/geosafe/js/jquery.dynatable.js
@@ -0,0 +1,1681 @@
+/*
+ * jQuery Dynatable plugin 0.3.1
+ *
+ * Copyright (c) 2014 Steve Schwartz (JangoSteve)
+ *
+ * Dual licensed under the AGPL and Proprietary licenses:
+ * http://www.dynatable.com/license/
+ *
+ * Date: Tue Jan 02 2014
+ */
+//
+
+(function($) {
+ var defaults,
+ mergeSettings,
+ dt,
+ Model,
+ modelPrototypes = {
+ dom: Dom,
+ domColumns: DomColumns,
+ records: Records,
+ recordsCount: RecordsCount,
+ processingIndicator: ProcessingIndicator,
+ state: State,
+ sorts: Sorts,
+ sortsHeaders: SortsHeaders,
+ queries: Queries,
+ inputsSearch: InputsSearch,
+ paginationPage: PaginationPage,
+ paginationPerPage: PaginationPerPage,
+ paginationLinks: PaginationLinks
+ },
+ utility,
+ build,
+ processAll,
+ initModel,
+ defaultRowWriter,
+ defaultCellWriter,
+ defaultAttributeWriter,
+ defaultAttributeReader;
+
+ //-----------------------------------------------------------------
+ // Cached plugin global defaults
+ //-----------------------------------------------------------------
+
+ defaults = {
+ features: {
+ paginate: true,
+ sort: true,
+ pushState: true,
+ search: true,
+ recordCount: true,
+ perPageSelect: true
+ },
+ table: {
+ defaultColumnIdStyle: 'camelCase',
+ columns: null,
+ headRowSelector: 'thead tr', // or e.g. tr:first-child
+ bodyRowSelector: 'tbody tr',
+ headRowClass: null
+ },
+ inputs: {
+ queries: null,
+ sorts: null,
+ multisort: ['ctrlKey', 'shiftKey', 'metaKey'],
+ page: null,
+ queryEvent: 'blur change',
+ recordCountTarget: null,
+ recordCountPlacement: 'after',
+ paginationLinkTarget: null,
+ paginationLinkPlacement: 'after',
+ paginationClass: 'dynatable-pagination-links',
+ paginationLinkClass: 'dynatable-page-link',
+ paginationPrevClass: 'dynatable-page-prev',
+ paginationNextClass: 'dynatable-page-next',
+ paginationActiveClass: 'dynatable-active-page',
+ paginationDisabledClass: 'dynatable-disabled-page',
+ paginationPrev: 'Previous',
+ paginationNext: 'Next',
+ paginationGap: [1,2,2,1],
+ searchTarget: null,
+ searchPlacement: 'before',
+ perPageTarget: null,
+ perPagePlacement: 'before',
+ perPageText: 'Show: ',
+ recordCountText: 'Showing ',
+ processingText: 'Processing...'
+ },
+ dataset: {
+ ajax: false,
+ ajaxUrl: null,
+ ajaxCache: null,
+ ajaxOnLoad: false,
+ ajaxMethod: 'GET',
+ ajaxDataType: 'json',
+ totalRecordCount: null,
+ queries: {},
+ queryRecordCount: null,
+ page: null,
+ perPageDefault: 10,
+ perPageOptions: [10,20,50,100],
+ sorts: {},
+ sortsKeys: null,
+ sortTypes: {},
+ records: null
+ },
+ writers: {
+ _rowWriter: defaultRowWriter,
+ _cellWriter: defaultCellWriter,
+ _attributeWriter: defaultAttributeWriter
+ },
+ readers: {
+ _rowReader: null,
+ _attributeReader: defaultAttributeReader
+ },
+ params: {
+ dynatable: 'dynatable',
+ queries: 'queries',
+ sorts: 'sorts',
+ page: 'page',
+ perPage: 'perPage',
+ offset: 'offset',
+ records: 'records',
+ record: null,
+ queryRecordCount: 'queryRecordCount',
+ totalRecordCount: 'totalRecordCount'
+ }
+ };
+
+ //-----------------------------------------------------------------
+ // Each dynatable instance inherits from this,
+ // set properties specific to instance
+ //-----------------------------------------------------------------
+
+ dt = {
+ init: function(element, options) {
+ this.settings = mergeSettings(options);
+ this.element = element;
+ this.$element = $(element);
+
+ // All the setup that doesn't require element or options
+ build.call(this);
+
+ return this;
+ },
+
+ process: function(skipPushState) {
+ processAll.call(this, skipPushState);
+ }
+ };
+
+ //-----------------------------------------------------------------
+ // Cached plugin global functions
+ //-----------------------------------------------------------------
+
+ mergeSettings = function(options) {
+ var newOptions = $.extend(true, {}, defaults, options);
+
+ // TODO: figure out a better way to do this.
+ // Doing `extend(true)` causes any elements that are arrays
+ // to merge the default and options arrays instead of overriding the defaults.
+ if (options) {
+ if (options.inputs) {
+ if (options.inputs.multisort) {
+ newOptions.inputs.multisort = options.inputs.multisort;
+ }
+ if (options.inputs.paginationGap) {
+ newOptions.inputs.paginationGap = options.inputs.paginationGap;
+ }
+ }
+ if (options.dataset && options.dataset.perPageOptions) {
+ newOptions.dataset.perPageOptions = options.dataset.perPageOptions;
+ }
+ }
+
+ return newOptions;
+ };
+
+ build = function() {
+ this.$element.trigger('dynatable:preinit', this);
+
+ for (model in modelPrototypes) {
+ if (modelPrototypes.hasOwnProperty(model)) {
+ var modelInstance = this[model] = new modelPrototypes[model](this, this.settings);
+ if (modelInstance.initOnLoad()) {
+ modelInstance.init();
+ }
+ }
+ }
+
+ this.$element.trigger('dynatable:init', this);
+
+ if (!this.settings.dataset.ajax || (this.settings.dataset.ajax && this.settings.dataset.ajaxOnLoad) || this.settings.features.paginate) {
+ this.process();
+ }
+ };
+
+ processAll = function(skipPushState) {
+ var data = {};
+
+ this.$element.trigger('dynatable:beforeProcess', data);
+
+ if (!$.isEmptyObject(this.settings.dataset.queries)) { data[this.settings.params.queries] = this.settings.dataset.queries; }
+ // TODO: Wrap this in a try/rescue block to hide the processing indicator and indicate something went wrong if error
+ this.processingIndicator.show();
+
+ if (this.settings.features.sort && !$.isEmptyObject(this.settings.dataset.sorts)) { data[this.settings.params.sorts] = this.settings.dataset.sorts; }
+ if (this.settings.features.paginate && this.settings.dataset.page) {
+ var page = this.settings.dataset.page,
+ perPage = this.settings.dataset.perPage;
+ data[this.settings.params.page] = page;
+ data[this.settings.params.perPage] = perPage;
+ data[this.settings.params.offset] = (page - 1) * perPage;
+ }
+ if (this.settings.dataset.ajaxData) { $.extend(data, this.settings.dataset.ajaxData); }
+
+ // If ajax, sends query to ajaxUrl with queries and sorts serialized and appended in ajax data
+ // otherwise, executes queries and sorts on in-page data
+ if (this.settings.dataset.ajax) {
+ var _this = this;
+ var options = {
+ type: _this.settings.dataset.ajaxMethod,
+ dataType: _this.settings.dataset.ajaxDataType,
+ data: data,
+ error: function(xhr, error) {
+ },
+ success: function(response) {
+ _this.$element.trigger('dynatable:ajax:success', response);
+ // Merge ajax results and meta-data into dynatables cached data
+ _this.records.updateFromJson(response);
+ // update table with new records
+ _this.dom.update();
+
+ if (!skipPushState && _this.state.initOnLoad()) {
+ _this.state.push(data);
+ }
+ },
+ complete: function() {
+ _this.processingIndicator.hide();
+ }
+ };
+ // Do not pass url to `ajax` options if blank
+ if (this.settings.dataset.ajaxUrl) {
+ options.url = this.settings.dataset.ajaxUrl;
+
+ // If ajaxUrl is blank, then we're using the current page URL,
+ // we need to strip out any query, sort, or page data controlled by dynatable
+ // that may have been in URL when page loaded, so that it doesn't conflict with
+ // what's passed in with the data ajax parameter
+ } else {
+ options.url = utility.refreshQueryString(window.location.href, {}, this.settings);
+ }
+ if (this.settings.dataset.ajaxCache !== null) { options.cache = this.settings.dataset.ajaxCache; }
+
+ $.ajax(options);
+ } else {
+ this.records.resetOriginal();
+ this.queries.run();
+ if (this.settings.features.sort) {
+ this.records.sort();
+ }
+ if (this.settings.features.paginate) {
+ this.records.paginate();
+ }
+ this.dom.update();
+ this.processingIndicator.hide();
+
+ if (!skipPushState && this.state.initOnLoad()) {
+ this.state.push(data);
+ }
+ }
+ this.$element.trigger('dynatable:afterProcess', data);
+ };
+
+ function defaultRowWriter(rowIndex, record, columns, cellWriter) {
+ var tr = '';
+
+ // grab the record's attribute for each column
+ for (var i = 0, len = columns.length; i < len; i++) {
+ tr += cellWriter(columns[i], record);
+ }
+
+ return '
' + tr + '
';
+ };
+
+ function defaultCellWriter(column, record) {
+ var html = column.attributeWriter(record),
+ td = '' + html + ' | ';
+ };
+
+ function defaultAttributeWriter(record) {
+ // `this` is the column object in settings.columns
+ // TODO: automatically convert common types, such as arrays and objects, to string
+ return record[this.id];
+ };
+
+ function defaultAttributeReader(cell, record) {
+ return $(cell).html();
+ };
+
+ //-----------------------------------------------------------------
+ // Dynatable object model prototype
+ // (all object models get these default functions)
+ //-----------------------------------------------------------------
+
+ Model = {
+ initOnLoad: function() {
+ return true;
+ },
+
+ init: function() {}
+ };
+
+ for (model in modelPrototypes) {
+ if (modelPrototypes.hasOwnProperty(model)) {
+ var modelPrototype = modelPrototypes[model];
+ modelPrototype.prototype = Model;
+ }
+ }
+
+ //-----------------------------------------------------------------
+ // Dynatable object models
+ //-----------------------------------------------------------------
+
+ function Dom(obj, settings) {
+ var _this = this;
+
+ // update table contents with new records array
+ // from query (whether ajax or not)
+ this.update = function() {
+ var rows = '',
+ columns = settings.table.columns,
+ rowWriter = settings.writers._rowWriter,
+ cellWriter = settings.writers._cellWriter;
+
+ obj.$element.trigger('dynatable:beforeUpdate', rows);
+
+ // loop through records
+ for (var i = 0, len = settings.dataset.records.length; i < len; i++) {
+ var record = settings.dataset.records[i],
+ tr = rowWriter(i, record, columns, cellWriter);
+ rows += tr;
+ }
+
+ // Appended dynatable interactive elements
+ if (settings.features.recordCount) {
+ $('#dynatable-record-count-' + obj.element.id).replaceWith(obj.recordsCount.create());
+ }
+ if (settings.features.paginate) {
+ $('#dynatable-pagination-links-' + obj.element.id).replaceWith(obj.paginationLinks.create());
+ if (settings.features.perPageSelect) {
+ $('#dynatable-per-page-' + obj.element.id).val(parseInt(settings.dataset.perPage));
+ }
+ }
+
+ // Sort headers functionality
+ if (settings.features.sort && columns) {
+ obj.sortsHeaders.removeAllArrows();
+ for (var i = 0, len = columns.length; i < len; i++) {
+ var column = columns[i],
+ sortedByColumn = utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; }),
+ value = settings.dataset.sorts[column.sorts[0]];
+
+ if (sortedByColumn) {
+ obj.$element.find('[data-dynatable-column="' + column.id + '"]').find('.dynatable-sort-header').each(function(){
+ if (value == 1) {
+ obj.sortsHeaders.appendArrowUp($(this));
+ } else {
+ obj.sortsHeaders.appendArrowDown($(this));
+ }
+ });
+ }
+ }
+ }
+
+ // Query search functionality
+ if (settings.inputs.queries || settings.features.search) {
+ var allQueries = settings.inputs.queries || $();
+ if (settings.features.search) {
+ allQueries = allQueries.add('#dynatable-query-search-' + obj.element.id);
+ }
+
+ allQueries.each(function() {
+ var $this = $(this),
+ q = settings.dataset.queries[$this.data('dynatable-query')];
+ $this.val(q || '');
+ });
+ }
+
+ obj.$element.find(settings.table.bodyRowSelector).remove();
+ obj.$element.append(rows);
+
+ obj.$element.trigger('dynatable:afterUpdate', rows);
+ };
+ };
+
+ function DomColumns(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return obj.$element.is('table');
+ };
+
+ this.init = function() {
+ settings.table.columns = [];
+ this.getFromTable();
+ };
+
+ // initialize table[columns] array
+ this.getFromTable = function() {
+ var $columns = obj.$element.find(settings.table.headRowSelector).children('th,td');
+ if ($columns.length) {
+ $columns.each(function(index){
+ _this.add($(this), index, true);
+ });
+ } else {
+ return $.error("Couldn't find any columns headers in '" + settings.table.headRowSelector + " th,td'. If your header row is different, specify the selector in the table: headRowSelector option.");
+ }
+ };
+
+ this.add = function($column, position, skipAppend, skipUpdate) {
+ var columns = settings.table.columns,
+ label = $column.text(),
+ id = $column.data('dynatable-column') || utility.normalizeText(label, settings.table.defaultColumnIdStyle),
+ dataSorts = $column.data('dynatable-sorts'),
+ sorts = dataSorts ? $.map(dataSorts.split(','), function(text) { return $.trim(text); }) : [id];
+
+ // If the column id is blank, generate an id for it
+ if ( !id ) {
+ this.generate($column);
+ id = $column.data('dynatable-column');
+ }
+ // Add column data to plugin instance
+ columns.splice(position, 0, {
+ index: position,
+ label: label,
+ id: id,
+ attributeWriter: settings.writers[id] || settings.writers._attributeWriter,
+ attributeReader: settings.readers[id] || settings.readers._attributeReader,
+ sorts: sorts,
+ hidden: $column.css('display') === 'none',
+ textAlign: $column.css('text-align')
+ });
+
+ // Modify header cell
+ $column
+ .attr('data-dynatable-column', id)
+ .addClass('dynatable-head');
+ if (settings.table.headRowClass) { $column.addClass(settings.table.headRowClass); }
+
+ // Append column header to table
+ if (!skipAppend) {
+ var domPosition = position + 1,
+ $sibling = obj.$element.find(settings.table.headRowSelector)
+ .children('th:nth-child(' + domPosition + '),td:nth-child(' + domPosition + ')').first(),
+ columnsAfter = columns.slice(position + 1, columns.length);
+
+ if ($sibling.length) {
+ $sibling.before($column);
+ // sibling column doesn't yet exist (maybe this is the last column in the header row)
+ } else {
+ obj.$element.find(settings.table.headRowSelector).append($column);
+ }
+
+ obj.sortsHeaders.attachOne($column.get());
+
+ // increment the index of all columns after this one that was just inserted
+ if (columnsAfter.length) {
+ for (var i = 0, len = columnsAfter.length; i < len; i++) {
+ columnsAfter[i].index += 1;
+ }
+ }
+
+ if (!skipUpdate) {
+ obj.dom.update();
+ }
+ }
+
+ return dt;
+ };
+
+ this.remove = function(columnIndexOrId) {
+ var columns = settings.table.columns,
+ length = columns.length;
+
+ if (typeof(columnIndexOrId) === "number") {
+ var column = columns[columnIndexOrId];
+ this.removeFromTable(column.id);
+ this.removeFromArray(columnIndexOrId);
+ } else {
+ // Traverse columns array in reverse order so that subsequent indices
+ // don't get messed up when we delete an item from the array in an iteration
+ for (var i = columns.length - 1; i >= 0; i--) {
+ var column = columns[i];
+
+ if (column.id === columnIndexOrId) {
+ this.removeFromTable(columnIndexOrId);
+ this.removeFromArray(i);
+ }
+ }
+ }
+
+ obj.dom.update();
+ };
+
+ this.removeFromTable = function(columnId) {
+ obj.$element.find(settings.table.headRowSelector).children('[data-dynatable-column="' + columnId + '"]').first()
+ .remove();
+ };
+
+ this.removeFromArray = function(index) {
+ var columns = settings.table.columns,
+ adjustColumns;
+ columns.splice(index, 1);
+ adjustColumns = columns.slice(index, columns.length);
+ for (var i = 0, len = adjustColumns.length; i < len; i++) {
+ adjustColumns[i].index -= 1;
+ }
+ };
+
+ this.generate = function($cell) {
+ var cell = $cell === undefined ? $(' | ') : $cell;
+ return this.attachGeneratedAttributes(cell);
+ };
+
+ this.attachGeneratedAttributes = function($cell) {
+ // Use increment to create unique column name that is the same each time the page is reloaded,
+ // in order to avoid errors with mismatched attribute names when loading cached `dataset.records` array
+ var increment = obj.$element.find(settings.table.headRowSelector).children('th[data-dynatable-generated]').length;
+ return $cell
+ .attr('data-dynatable-column', 'dynatable-generated-' + increment) //+ utility.randomHash(),
+ .attr('data-dynatable-no-sort', 'true')
+ .attr('data-dynatable-generated', increment);
+ };
+ };
+
+ function Records(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return !settings.dataset.ajax;
+ };
+
+ this.init = function() {
+ if (settings.dataset.records === null) {
+ settings.dataset.records = this.getFromTable();
+
+ if (!settings.dataset.queryRecordCount) {
+ settings.dataset.queryRecordCount = this.count();
+ }
+
+ if (!settings.dataset.totalRecordCount){
+ settings.dataset.totalRecordCount = settings.dataset.queryRecordCount;
+ }
+ }
+
+ // Create cache of original full recordset (unpaginated and unqueried)
+ settings.dataset.originalRecords = $.extend(true, [], settings.dataset.records);
+ };
+
+ // merge ajax response json with cached data including
+ // meta-data and records
+ this.updateFromJson = function(data) {
+ var records;
+ if (settings.params.records === "_root") {
+ records = data;
+ } else if (settings.params.records in data) {
+ records = data[settings.params.records];
+ }
+ if (settings.params.record) {
+ var len = records.length - 1;
+ for (var i = 0; i < len; i++) {
+ records[i] = records[i][settings.params.record];
+ }
+ }
+ if (settings.params.queryRecordCount in data) {
+ settings.dataset.queryRecordCount = data[settings.params.queryRecordCount];
+ }
+ if (settings.params.totalRecordCount in data) {
+ settings.dataset.totalRecordCount = data[settings.params.totalRecordCount];
+ }
+ settings.dataset.records = records;
+ };
+
+ // For really advanced sorting,
+ // see http://james.padolsey.com/javascript/sorting-elements-with-jquery/
+ this.sort = function() {
+ var sort = [].sort,
+ sorts = settings.dataset.sorts,
+ sortsKeys = settings.dataset.sortsKeys,
+ sortTypes = settings.dataset.sortTypes;
+
+ var sortFunction = function(a, b) {
+ var comparison;
+ if ($.isEmptyObject(sorts)) {
+ comparison = obj.sorts.functions['originalPlacement'](a, b);
+ } else {
+ for (var i = 0, len = sortsKeys.length; i < len; i++) {
+ var attr = sortsKeys[i],
+ direction = sorts[attr],
+ sortType = sortTypes[attr] || obj.sorts.guessType(a, b, attr);
+ comparison = obj.sorts.functions[sortType](a, b, attr, direction);
+ // Don't need to sort any further unless this sort is a tie between a and b,
+ // so break the for loop unless tied
+ if (comparison !== 0) { break; }
+ }
+ }
+ return comparison;
+ }
+
+ return sort.call(settings.dataset.records, sortFunction);
+ };
+
+ this.paginate = function() {
+ var bounds = this.pageBounds(),
+ first = bounds[0], last = bounds[1];
+ settings.dataset.records = settings.dataset.records.slice(first, last);
+ };
+
+ this.resetOriginal = function() {
+ settings.dataset.records = settings.dataset.originalRecords || [];
+ };
+
+ this.pageBounds = function() {
+ var page = settings.dataset.page || 1,
+ first = (page - 1) * settings.dataset.perPage,
+ last = Math.min(first + settings.dataset.perPage, settings.dataset.queryRecordCount);
+ return [first,last];
+ };
+
+ // get initial recordset to populate table
+ // if ajax, call ajaxUrl
+ // otherwise, initialize from in-table records
+ this.getFromTable = function() {
+ var records = [],
+ columns = settings.table.columns,
+ tableRecords = obj.$element.find(settings.table.bodyRowSelector);
+
+ tableRecords.each(function(index){
+ var record = {};
+ record['dynatable-original-index'] = index;
+ $(this).find('th,td').each(function(index) {
+ if (columns[index] === undefined) {
+ // Header cell didn't exist for this column, so let's generate and append
+ // a new header cell with a randomly generated name (so we can store and
+ // retrieve the contents of this column for each record)
+ obj.domColumns.add(obj.domColumns.generate(), columns.length, false, true); // don't skipAppend, do skipUpdate
+ }
+ var value = columns[index].attributeReader(this, record),
+ attr = columns[index].id;
+
+ // If value from table is HTML, let's get and cache the text equivalent for
+ // the default string sorting, since it rarely makes sense for sort headers
+ // to sort based on HTML tags.
+ if (typeof(value) === "string" && value.match(/\s*\<.+\>/)) {
+ if (! record['dynatable-sortable-text']) {
+ record['dynatable-sortable-text'] = {};
+ }
+ record['dynatable-sortable-text'][attr] = $.trim($('').html(value).text());
+ }
+
+ record[attr] = value;
+ });
+ // Allow configuration function which alters record based on attributes of
+ // table row (e.g. from html5 data- attributes)
+ if (typeof(settings.readers._rowReader) === "function") {
+ settings.readers._rowReader(index, this, record);
+ }
+ records.push(record);
+ });
+ return records; // 1st row is header
+ };
+
+ // count records from table
+ this.count = function() {
+ return settings.dataset.records.length;
+ };
+ };
+
+ function RecordsCount(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.recordCount;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var recordsShown = obj.records.count(),
+ recordsQueryCount = settings.dataset.queryRecordCount,
+ recordsTotal = settings.dataset.totalRecordCount,
+ text = settings.inputs.recordCountText,
+ collection_name = settings.params.records;
+
+ if (recordsShown < recordsQueryCount && settings.features.paginate) {
+ var bounds = obj.records.pageBounds();
+ text += "" + (bounds[0] + 1) + " to " + bounds[1] + " of ";
+ } else if (recordsShown === recordsQueryCount && settings.features.paginate) {
+ text += recordsShown + " of ";
+ }
+ text += recordsQueryCount + " " + collection_name;
+ if (recordsQueryCount < recordsTotal) {
+ text += " (filtered from " + recordsTotal + " total records)";
+ }
+
+ return $('', {
+ id: 'dynatable-record-count-' + obj.element.id,
+ 'class': 'dynatable-record-count',
+ html: text
+ });
+ };
+
+ this.attach = function() {
+ var $target = settings.inputs.recordCountTarget ? $(settings.inputs.recordCountTarget) : obj.$element;
+ $target[settings.inputs.recordCountPlacement](this.create());
+ };
+ };
+
+ function ProcessingIndicator(obj, settings) {
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var $processing = $('', {
+ html: '' + settings.inputs.processingText + '',
+ id: 'dynatable-processing-' + obj.element.id,
+ 'class': 'dynatable-processing',
+ style: 'position: absolute; display: none;'
+ });
+
+ return $processing;
+ };
+
+ this.position = function() {
+ var $processing = $('#dynatable-processing-' + obj.element.id),
+ $span = $processing.children('span'),
+ spanHeight = $span.outerHeight(),
+ spanWidth = $span.outerWidth(),
+ $covered = obj.$element,
+ offset = $covered.offset(),
+ height = $covered.outerHeight(), width = $covered.outerWidth();
+
+ $processing
+ .offset({left: offset.left, top: offset.top})
+ .width(width)
+ .height(height)
+ $span
+ .offset({left: offset.left + ( (width - spanWidth) / 2 ), top: offset.top + ( (height - spanHeight) / 2 )});
+
+ return $processing;
+ };
+
+ this.attach = function() {
+ obj.$element.before(this.create());
+ };
+
+ this.show = function() {
+ $('#dynatable-processing-' + obj.element.id).show();
+ this.position();
+ };
+
+ this.hide = function() {
+ $('#dynatable-processing-' + obj.element.id).hide();
+ };
+ };
+
+ function State(obj, settings) {
+ this.initOnLoad = function() {
+ // Check if pushState option is true, and if browser supports it
+ return settings.features.pushState && history.pushState;
+ };
+
+ this.init = function() {
+ window.onpopstate = function(event) {
+ if (event.state && event.state.dynatable) {
+ obj.state.pop(event);
+ }
+ }
+ };
+
+ this.push = function(data) {
+ var urlString = window.location.search,
+ urlOptions,
+ path,
+ params,
+ hash,
+ newParams,
+ cacheStr,
+ cache,
+ // replaceState on initial load, then pushState after that
+ firstPush = !(window.history.state && window.history.state.dynatable),
+ pushFunction = firstPush ? 'replaceState' : 'pushState';
+
+ if (urlString && /^\?/.test(urlString)) { urlString = urlString.substring(1); }
+ $.extend(urlOptions, data);
+
+ params = utility.refreshQueryString(urlString, data, settings);
+ if (params) { params = '?' + params; }
+ hash = window.location.hash;
+ path = window.location.pathname;
+
+ obj.$element.trigger('dynatable:push', data);
+
+ cache = { dynatable: { dataset: settings.dataset } };
+ if (!firstPush) { cache.dynatable.scrollTop = $(window).scrollTop(); }
+ cacheStr = JSON.stringify(cache);
+
+ // Mozilla has a 640k char limit on what can be stored in pushState.
+ // See "limit" in https://developer.mozilla.org/en/DOM/Manipulating_the_browser_history#The_pushState().C2.A0method
+ // and "dataStr.length" in http://wine.git.sourceforge.net/git/gitweb.cgi?p=wine/wine-gecko;a=patch;h=43a11bdddc5fc1ff102278a120be66a7b90afe28
+ //
+ // Likewise, other browsers may have varying (undocumented) limits.
+ // Also, Firefox's limit can be changed in about:config as browser.history.maxStateObjectSize
+ // Since we don't know what the actual limit will be in any given situation, we'll just try caching and rescue
+ // any exceptions by retrying pushState without caching the records.
+ //
+ // I have absolutely no idea why perPageOptions suddenly becomes an array-like object instead of an array,
+ // but just recently, this started throwing an error if I don't convert it:
+ // 'Uncaught Error: DATA_CLONE_ERR: DOM Exception 25'
+ cache.dynatable.dataset.perPageOptions = $.makeArray(cache.dynatable.dataset.perPageOptions);
+
+ try {
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
+ } catch(error) {
+ // Make cached records = null, so that `pop` will rerun process to retrieve records
+ cache.dynatable.dataset.records = null;
+ window.history[pushFunction](cache, "Dynatable state", path + params + hash);
+ }
+ };
+
+ this.pop = function(event) {
+ var data = event.state.dynatable;
+ settings.dataset = data.dataset;
+
+ if (data.scrollTop) { $(window).scrollTop(data.scrollTop); }
+
+ // If dataset.records is cached from pushState
+ if ( data.dataset.records ) {
+ obj.dom.update();
+ } else {
+ obj.process(true);
+ }
+ };
+ };
+
+ function Sorts(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.sort;
+ };
+
+ this.init = function() {
+ var sortsUrl = window.location.search.match(new RegExp(settings.params.sorts + '[^&=]*=[^&]*', 'g'));
+ settings.dataset.sorts = sortsUrl ? utility.deserialize(sortsUrl)[settings.params.sorts] : {};
+ settings.dataset.sortsKeys = sortsUrl ? utility.keysFromObject(settings.dataset.sorts) : [];
+ };
+
+ this.add = function(attr, direction) {
+ var sortsKeys = settings.dataset.sortsKeys,
+ index = $.inArray(attr, sortsKeys);
+ settings.dataset.sorts[attr] = direction;
+ if (index === -1) { sortsKeys.push(attr); }
+ return dt;
+ };
+
+ this.remove = function(attr) {
+ var sortsKeys = settings.dataset.sortsKeys,
+ index = $.inArray(attr, sortsKeys);
+ delete settings.dataset.sorts[attr];
+ if (index !== -1) { sortsKeys.splice(index, 1); }
+ return dt;
+ };
+
+ this.clear = function() {
+ settings.dataset.sorts = {};
+ settings.dataset.sortsKeys.length = 0;
+ };
+
+ // Try to intelligently guess which sort function to use
+ // based on the type of attribute values.
+ // Consider using something more robust than `typeof` (http://javascriptweblog.wordpress.com/2011/08/08/fixing-the-javascript-typeof-operator/)
+ this.guessType = function(a, b, attr) {
+ var types = {
+ string: 'string',
+ number: 'number',
+ 'boolean': 'number',
+ object: 'number' // dates and null values are also objects, this works...
+ },
+ attrType = a[attr] ? typeof(a[attr]) : typeof(b[attr]),
+ type = types[attrType] || 'number';
+ return type;
+ };
+
+ // Built-in sort functions
+ // (the most common use-cases I could think of)
+ this.functions = {
+ number: function(a, b, attr, direction) {
+ return a[attr] === b[attr] ? 0 : (direction > 0 ? a[attr] - b[attr] : b[attr] - a[attr]);
+ },
+ string: function(a, b, attr, direction) {
+ var aAttr = (a['dynatable-sortable-text'] && a['dynatable-sortable-text'][attr]) ? a['dynatable-sortable-text'][attr] : a[attr],
+ bAttr = (b['dynatable-sortable-text'] && b['dynatable-sortable-text'][attr]) ? b['dynatable-sortable-text'][attr] : b[attr],
+ comparison;
+ aAttr = aAttr.toLowerCase();
+ bAttr = bAttr.toLowerCase();
+ comparison = aAttr === bAttr ? 0 : (direction > 0 ? aAttr > bAttr : bAttr > aAttr);
+ // force false boolean value to -1, true to 1, and tie to 0
+ return comparison === false ? -1 : (comparison - 0);
+ },
+ originalPlacement: function(a, b) {
+ return a['dynatable-original-index'] - b['dynatable-original-index'];
+ }
+ };
+ };
+
+ // turn table headers into links which add sort to sorts array
+ function SortsHeaders(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.sort;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function(cell) {
+ var $cell = $(cell),
+ $link = $('', {
+ 'class': 'dynatable-sort-header',
+ href: '#',
+ html: $cell.html()
+ }),
+ id = $cell.data('dynatable-column'),
+ column = utility.findObjectInArray(settings.table.columns, {id: id});
+
+ $link.bind('click', function(e) {
+ _this.toggleSort(e, $link, column);
+ obj.process();
+
+ e.preventDefault();
+ });
+
+ if (this.sortedByColumn($link, column)) {
+ if (this.sortedByColumnValue(column) == 1) {
+ this.appendArrowUp($link);
+ } else {
+ this.appendArrowDown($link);
+ }
+ }
+
+ return $link;
+ };
+
+ this.removeAll = function() {
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
+ _this.removeAllArrows();
+ _this.removeOne(this);
+ });
+ };
+
+ this.removeOne = function(cell) {
+ var $cell = $(cell),
+ $link = $cell.find('.dynatable-sort-header');
+ if ($link.length) {
+ var html = $link.html();
+ $link.remove();
+ $cell.html($cell.html() + html);
+ }
+ };
+
+ this.attach = function() {
+ obj.$element.find(settings.table.headRowSelector).children('th,td').each(function(){
+ _this.attachOne(this);
+ });
+ };
+
+ this.attachOne = function(cell) {
+ var $cell = $(cell);
+ if (!$cell.data('dynatable-no-sort')) {
+ $cell.html(this.create(cell));
+ }
+ };
+
+ this.appendArrowUp = function($link) {
+ this.removeArrow($link);
+ $link.append(" ▲");
+ };
+
+ this.appendArrowDown = function($link) {
+ this.removeArrow($link);
+ $link.append(" ▼");
+ };
+
+ this.removeArrow = function($link) {
+ // Not sure why `parent()` is needed, the arrow should be inside the link from `append()` above
+ $link.find('.dynatable-arrow').remove();
+ };
+
+ this.removeAllArrows = function() {
+ obj.$element.find('.dynatable-arrow').remove();
+ };
+
+ this.toggleSort = function(e, $link, column) {
+ var sortedByColumn = this.sortedByColumn($link, column),
+ value = this.sortedByColumnValue(column);
+ // Clear existing sorts unless this is a multisort event
+ if (!settings.inputs.multisort || !utility.anyMatch(e, settings.inputs.multisort, function(evt, key) { return e[key]; })) {
+ this.removeAllArrows();
+ obj.sorts.clear();
+ }
+
+ // If sorts for this column are already set
+ if (sortedByColumn) {
+ // If ascending, then make descending
+ if (value == 1) {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.add(column.sorts[i], -1);
+ }
+ this.appendArrowDown($link);
+ // If descending, remove sort
+ } else {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.remove(column.sorts[i]);
+ }
+ this.removeArrow($link);
+ }
+ // Otherwise, if not already set, set to ascending
+ } else {
+ for (var i = 0, len = column.sorts.length; i < len; i++) {
+ obj.sorts.add(column.sorts[i], 1);
+ }
+ this.appendArrowUp($link);
+ }
+ };
+
+ this.sortedByColumn = function($link, column) {
+ return utility.allMatch(settings.dataset.sorts, column.sorts, function(sorts, sort) { return sort in sorts; });
+ };
+
+ this.sortedByColumnValue = function(column) {
+ return settings.dataset.sorts[column.sorts[0]];
+ };
+ };
+
+ function Queries(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.inputs.queries || settings.features.search;
+ };
+
+ this.init = function() {
+ var queriesUrl = window.location.search.match(new RegExp(settings.params.queries + '[^&=]*=[^&]*', 'g'));
+
+ settings.dataset.queries = queriesUrl ? utility.deserialize(queriesUrl)[settings.params.queries] : {};
+ if (settings.dataset.queries === "") { settings.dataset.queries = {}; }
+
+ if (settings.inputs.queries) {
+ this.setupInputs();
+ }
+ };
+
+ this.add = function(name, value) {
+ // reset to first page since query will change records
+ if (settings.features.paginate) {
+ settings.dataset.page = 1;
+ }
+ settings.dataset.queries[name] = value;
+ return dt;
+ };
+
+ this.remove = function(name) {
+ delete settings.dataset.queries[name];
+ return dt;
+ };
+
+ this.run = function() {
+ for (query in settings.dataset.queries) {
+ if (settings.dataset.queries.hasOwnProperty(query)) {
+ var value = settings.dataset.queries[query];
+ if (_this.functions[query] === undefined) {
+ // Try to lazily evaluate query from column names if not explicitly defined
+ var queryColumn = utility.findObjectInArray(settings.table.columns, {id: query});
+ if (queryColumn) {
+ _this.functions[query] = function(record, queryValue) {
+ return record[query] == queryValue;
+ };
+ } else {
+ $.error("Query named '" + query + "' called, but not defined in queries.functions");
+ continue; // to skip to next query
+ }
+ }
+ // collect all records that return true for query
+ settings.dataset.records = $.map(settings.dataset.records, function(record) {
+ return _this.functions[query](record, value) ? record : null;
+ });
+ }
+ }
+ settings.dataset.queryRecordCount = obj.records.count();
+ };
+
+ // Shortcut for performing simple query from built-in search
+ this.runSearch = function(q) {
+ var origQueries = $.extend({}, settings.dataset.queries);
+ if (q) {
+ this.add('search', q);
+ } else {
+ this.remove('search');
+ }
+ if (!utility.objectsEqual(settings.dataset.queries, origQueries)) {
+ obj.process();
+ }
+ };
+
+ this.setupInputs = function() {
+ settings.inputs.queries.each(function() {
+ var $this = $(this),
+ event = $this.data('dynatable-query-event') || settings.inputs.queryEvent,
+ query = $this.data('dynatable-query') || $this.attr('name') || this.id,
+ queryFunction = function(e) {
+ var q = $(this).val();
+ if (q === "") { q = undefined; }
+ if (q === settings.dataset.queries[query]) { return false; }
+ if (q) {
+ _this.add(query, q);
+ } else {
+ _this.remove(query);
+ }
+ obj.process();
+ e.preventDefault();
+ };
+
+ $this
+ .attr('data-dynatable-query', query)
+ .bind(event, queryFunction)
+ .bind('keypress', function(e) {
+ if (e.which == 13) {
+ queryFunction.call(this, e);
+ }
+ });
+
+ if (settings.dataset.queries[query]) { $this.val(decodeURIComponent(settings.dataset.queries[query])); }
+ });
+ };
+
+ // Query functions for in-page querying
+ // each function should take a record and a value as input
+ // and output true of false as to whether the record is a match or not
+ this.functions = {
+ search: function(record, queryValue) {
+ var contains = false;
+ // Loop through each attribute of record
+ for (attr in record) {
+ if (record.hasOwnProperty(attr)) {
+ var attrValue = record[attr];
+ if (typeof(attrValue) === "string" && attrValue.toLowerCase().indexOf(queryValue.toLowerCase()) !== -1) {
+ contains = true;
+ // Don't need to keep searching attributes once found
+ break;
+ } else {
+ continue;
+ }
+ }
+ }
+ return contains;
+ }
+ };
+ };
+
+ function InputsSearch(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.search;
+ };
+
+ this.init = function() {
+ this.attach();
+ };
+
+ this.create = function() {
+ var $search = $('', {
+ type: 'search',
+ id: 'dynatable-query-search-' + obj.element.id,
+ 'data-dynatable-query': 'search',
+ value: settings.dataset.queries.search
+ }),
+ $searchSpan = $('', {
+ id: 'dynatable-search-' + obj.element.id,
+ 'class': 'dynatable-search',
+ text: 'Search: '
+ }).append($search);
+
+ $search
+ .bind(settings.inputs.queryEvent, function() {
+ obj.queries.runSearch($(this).val());
+ })
+ .bind('keypress', function(e) {
+ if (e.which == 13) {
+ obj.queries.runSearch($(this).val());
+ e.preventDefault();
+ }
+ });
+ return $searchSpan;
+ };
+
+ this.attach = function() {
+ var $target = settings.inputs.searchTarget ? $(settings.inputs.searchTarget) : obj.$element;
+ $target[settings.inputs.searchPlacement](this.create());
+ };
+ };
+
+ // provide a public function for selecting page
+ function PaginationPage(obj, settings) {
+ this.initOnLoad = function() {
+ return settings.features.paginate;
+ };
+
+ this.init = function() {
+ var pageUrl = window.location.search.match(new RegExp(settings.params.page + '=([^&]*)'));
+ // If page is present in URL parameters and pushState is enabled
+ // (meaning that it'd be possible for dynatable to have put the
+ // page parameter in the URL)
+ if (pageUrl && settings.features.pushState) {
+ this.set(pageUrl[1]);
+ } else {
+ this.set(1);
+ }
+ };
+
+ this.set = function(page) {
+ settings.dataset.page = parseInt(page, 10);
+ }
+ };
+
+ function PaginationPerPage(obj, settings) {
+ var _this = this;
+
+ this.initOnLoad = function() {
+ return settings.features.paginate;
+ };
+
+ this.init = function() {
+ var perPageUrl = window.location.search.match(new RegExp(settings.params.perPage + '=([^&]*)'));
+
+ // If perPage is present in URL parameters and pushState is enabled
+ // (meaning that it'd be possible for dynatable to have put the
+ // perPage parameter in the URL)
+ if (perPageUrl && settings.features.pushState) {
+ // Don't reset page to 1 on init, since it might override page
+ // set on init from URL
+ this.set(perPageUrl[1], true);
+ } else {
+ this.set(settings.dataset.perPageDefault, true);
+ }
+
+ if (settings.features.perPageSelect) {
+ this.attach();
+ }
+ };
+
+ this.create = function() {
+ var $select = $('