diff --git a/js/h5p-data-view.js b/js/h5p-data-view.js new file mode 100644 index 0000000..323290a --- /dev/null +++ b/js/h5p-data-view.js @@ -0,0 +1,255 @@ +var H5PDataView = (function ($) { + + /** + * Initialize a new H5P data view. + * + * @class + * @param {Object} container + * Element to clear out and append to. + * @param {String} source + * URL to get data from. Data format: {num: 123, rows:[[1,2,3],[2,4,6]]} + * @param {Array} headers + * List with column headers. Can be strings or objects with options like + * "text" and "sortable". E.g. + * [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] + * @param {Object} l10n + * Localization / translations. e.g. + * { + * loading: 'Loading data.', + * ajaxFailed: 'Failed to load data.', + * noData: "There's no data available that matches your criteria.", + * currentPage: 'Page $current of $total', + * nextPage: 'Next page', + * previousPage: 'Previous page', + * search: 'Search' + * } + * @param {Object} classes + * Custom html classes to use on elements. + * e.g. {tableClass: 'fixed'}. + * @param {Array} filters + * Make it possible to filter/search in the given column. + * e.g. [null, true, null, null] will make it possible to do a text + * search in column 2. + * @param {Function} loaded + * Callback for when data has been loaded. + */ + function H5PDataView(container, source, headers, l10n, classes, filters, loaded) { + var self = this; + + self.$container = $(container).addClass('h5p-data-view').html(''); + + self.source = source; + self.headers = headers; + self.l10n = l10n; + self.classes = (classes === undefined ? {} : classes); + self.filters = (filters === undefined ? [] : filters); + self.loaded = loaded; + + self.limit = 20; + self.offset = 0; + self.filterOn = []; + + self.loadData(); + } + + /** + * Load data from source URL. + * + * @public + */ + H5PDataView.prototype.loadData = function () { + var self = this; + + // Throbb + self.setMessage(H5PUtils.throbber(self.l10n.loading)); + + // Create URL + var url = self.source; + url += (url.indexOf('?') === -1 ? '?' : '&') + 'offset=' + self.offset + '&limit=' + self.limit; + + // Add sorting + if (self.sortBy !== undefined && self.sortDir !== undefined) { + url += '&sortBy=' + self.sortBy + '&sortDir=' + self.sortDir; + } + + // Add filters + var filtering; + for (var i = 0; i < self.filterOn.length; i++) { + if (self.filterOn[i] === undefined) { + continue; + } + + filtering = true; + url += '&filters[' + i + ']=' + encodeURIComponent(self.filterOn[i]); + } + + // Fire ajax request + $.ajax({ + dataType: 'json', + cache: true, + url: url + }).fail(function () { + // Error handling + self.setMessage($('

', {text: self.l10n.ajaxFailed})); + }).done(function (data) { + if (!data.rows.length) { + self.setMessage($('

', {text: filtering ? self.l10n.noData : self.l10n.empty})); + } + else { + // Update table data + self.updateTable(data.rows); + } + + // Update pagination widget + self.updatePagination(data.num); + + if (self.loaded !== undefined) { + self.loaded(); + } + }); + }; + + /** + * Display the given message to the user. + * + * @public + * @param {jQuery} $message wrapper with message + */ + H5PDataView.prototype.setMessage = function ($message) { + var self = this; + + if (self.table === undefined) { + self.$container.html('').append($message); + } + else { + self.table.setBody($message); + } + }; + + /** + * Update table data. + * + * @public + * @param {Array} rows + */ + H5PDataView.prototype.updateTable = function (rows) { + var self = this; + + if (self.table === undefined) { + // Clear out container + self.$container.html(''); + + // Add filters + self.addFilters(); + + // Create new table + self.table = new H5PUtils.Table(self.classes, self.headers); + self.table.setHeaders(self.headers, function (col, dir) { + // Sorting column or direction has changed callback. + self.sortBy = col; + self.sortDir = dir; + self.loadData(); + }); + self.table.appendTo(self.$container); + } + + // Add/update rows + self.table.setRows(rows); + }; + + /** + * Update pagination widget. + * + * @public + * @param {Number} num size of data collection + */ + H5PDataView.prototype.updatePagination = function (num) { + var self = this; + + if (self.pagination === undefined) { + // Create new widget + var $pagerContainer = $('

', {'class': 'h5p-pagination'}); + self.pagination = new H5PUtils.Pagination(num, self.limit, function (offset) { + // Handle page changes in pagination widget + self.offset = offset; + self.loadData(); + }, self.l10n); + + self.pagination.appendTo($pagerContainer); + self.table.setFoot($pagerContainer); + } + else { + // Update existing widget + self.pagination.update(num, self.limit); + } + }; + + /** + * Add filters. + * + * @public + */ + H5PDataView.prototype.addFilters = function () { + var self = this; + + for (var i = 0; i < self.filters.length; i++) { + if (self.filters[i] === true) { + // Add text input filter for col i + self.addTextFilter(i); + } + } + }; + + /** + * Add text filter for given col num. + + * @public + * @param {Number} col + */ + H5PDataView.prototype.addTextFilter = function (col) { + var self = this; + + /** + * Find input value and filter on it. + * @private + */ + var search = function () { + var filterOn = $input.val().replace(/^\s+|\s+$/g, ''); + if (filterOn === '') { + filterOn = undefined; + } + if (filterOn !== self.filterOn[col]) { + self.filterOn[col] = filterOn; + self.loadData(); + } + }; + + // Add text field for filtering + var typing; + var $input = $('', { + type: 'text', + placeholder: self.l10n.search, + on: { + 'blur': function () { + clearTimeout(typing); + search(); + }, + 'keyup': function (event) { + if (event.keyCode === 13) { + clearTimeout(typing); + search(); + return false; + } + else { + clearTimeout(typing); + typing = setTimeout(function () { + search(); + }, 500); + } + } + } + }).appendTo(self.$container); + }; + + return H5PDataView; +})(H5P.jQuery); diff --git a/js/h5p-utils.js b/js/h5p-utils.js index 0db73a7..00af7bc 100644 --- a/js/h5p-utils.js +++ b/js/h5p-utils.js @@ -64,7 +64,7 @@ var H5PUtils = H5PUtils || {}; return $field; }; - + /** * Replaces placeholder fields in translation strings * @@ -130,4 +130,353 @@ var H5PUtils = H5PUtils || {}; return $container; }; + /** + * Generic table class with useful helpers. + * + * @class + * @param {Object} classes + * Custom html classes to use on elements. + * e.g. {tableClass: 'fixed'}. + */ + H5PUtils.Table = function (classes) { + var numCols; + var sortByCol; + var $sortCol; + var sortCol; + var sortDir; + + // Create basic table + var tableOptions = {}; + if (classes.table !== undefined) { + tableOptions['class'] = classes.table; + } + var $table = $('', tableOptions); + var $thead = $('').appendTo($table); + var $tfoot = $('').appendTo($table); + var $tbody = $('').appendTo($table); + + /** + * Add columns to given table row. + * + * @private + * @param {jQuery} $tr Table row + * @param {(String|Object)} col Column properties + * @param {Number} id Used to seperate the columns + */ + var addCol = function ($tr, col, id) { + var options = { + on: {} + }; + + if (!(col instanceof Object)) { + options.text = col; + } + else { + if (col.text !== undefined) { + options.text = col.text; + } + if (col.class !== undefined) { + options.class = col.class; + } + + if (sortByCol !== undefined && col.sortable === true) { + // Make sortable + options.role = 'button'; + options.tabIndex = 1; + + // This is the first sortable column, use as default sort + if (sortCol === undefined) { + sortCol = id; + sortDir = 0; + options['class'] = 'h5p-sort'; + } + + options.on.click = function () { + sort($th, id); + }; + } + } + + // Append + var $th = $(''); + var $tr = $('').appendTo($newThead); + for (var i = 0; i < cols.length; i++) { + addCol($tr, cols[i], i); + } + + // Update DOM + $thead.replaceWith($newThead); + $thead = $newThead; + }; + + /** + * Set table rows. + * + * @public + * @param {Array} rows Table rows with cols: [[1,'hello',3],[2,'asd',6]] + */ + this.setRows = function (rows) { + var $newTbody = $(''); + + for (var i = 0; i < rows.length; i++) { + var $tr = $('').appendTo($newTbody); + + for (var j = 0; j < rows[i].length; j++) { + $(''); + var $tr = $('').appendTo($newTbody); + $(''); + var $tr = $('').appendTo($newTfoot); + $('
', options).appendTo($tr); + if (sortCol === id) { + $sortCol = $th; // Default sort column + } + }; + + /** + * Updates the UI when a column header has been clicked. + * Triggers sorting callback. + * + * @private + * @param {jQuery} $th Table header + * @param {Number} id Used to seperate the columns + */ + var sort = function ($th, id) { + if (id === sortCol) { + // Change sorting direction + if (sortDir === 0) { + sortDir = 1; + $th.addClass('h5p-reverse'); + } + else { + sortDir = 0; + $th.removeClass('h5p-reverse'); + } + } + else { + // Change sorting column + $sortCol.removeClass('h5p-sort').removeClass('h5p-reverse'); + $sortCol = $th.addClass('h5p-sort'); + sortCol = id; + sortDir = 0; + } + + sortByCol(sortCol, sortDir); + }; + + /** + * Set table headers. + * + * @public + * @param {Array} cols + * Table header data. Can be strings or objects with options like + * "text" and "sortable". E.g. + * [{text: 'Col 1', sortable: true}, 'Col 2', 'Col 3'] + * @param {Function} sort Callback which is runned when sorting changes + */ + this.setHeaders = function (cols, sort) { + numCols = cols.length; + sortByCol = sort; + + // Create new head + var $newThead = $('
', { + html: rows[i][j] + }).appendTo($tr); + } + } + + $tbody.replaceWith($newTbody); + $tbody = $newTbody; + }; + + /** + * Set custom table body content. This can be a message or a throbber. + * Will cover all table columns. + * + * @public + * @param {jQuery} $content Custom content + */ + this.setBody = function ($content) { + var $newTbody = $('
', { + colspan: numCols + }).append($content).appendTo($tr); + $tbody.replaceWith($newTbody); + $tbody = $newTbody; + }; + + /** + * Set custom table foot content. This can be a pagination widget. + * Will cover all table columns. + * + * @public + * @param {jQuery} $content Custom content + */ + this.setFoot = function ($content) { + var $newTfoot = $('
', { + colspan: numCols + }).append($content).appendTo($tr); + $tfoot.replaceWith($newTfoot); + }; + + + /** + * Appends the table to the given container. + * + * @public + * @param {jQuery} $container + */ + this.appendTo = function ($container) { + $table.appendTo($container); + }; + }; + + /** + * Generic pagination class. Creates a useful pagination widget. + * + * @class + * @param {Number} num Total number of items to pagiate. + * @param {Number} limit Number of items to dispaly per page. + * @param {Function} goneTo + * Callback which is fired when the user wants to go to another page. + * @param {Object} l10n + * Localization / translations. e.g. + * { + * currentPage: 'Page $current of $total', + * nextPage: 'Next page', + * previousPage: 'Previous page' + * } + */ + H5PUtils.Pagination = function (num, limit, goneTo, l10n) { + var current = 0; + var pages = Math.ceil(num / limit); + + // Create components + + // Previous button + var $left = $('