diff --git a/h5p.classes.php b/h5p.classes.php index 2df9901..398dbc7 100644 --- a/h5p.classes.php +++ b/h5p.classes.php @@ -1535,9 +1535,9 @@ class H5PCore { public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff otf webm mp4 ogg mp3 txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile'; public static $defaultLibraryWhitelistExtras = 'js css'; + public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $path, $development_mode, $h5pD, $disableFileCheck; const SECONDS_IN_WEEK = 604800; - public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $path, $development_mode, $h5pD; private $exportEnabled; /** @@ -2363,6 +2363,10 @@ class H5PContentValidator { * FALSE if one or more files fail validation. Error message should be set accordingly by validator. */ public function validateContentFiles($contentPath, $isLibrary = FALSE) { + if ($this->h5pC->disableFileCheck === TRUE) { + return TRUE; + } + // Scan content directory for files, recurse into sub directories. $files = array_diff(scandir($contentPath), array('.','..')); $valid = TRUE; 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 = $('