diff --git a/app/controllers/course/files.php b/app/controllers/course/files.php index d98ef9a606e379b456bb32869f51f2887f6539ca..e649a177ba257778a96aca375dee8ad661dbe78b 100644 --- a/app/controllers/course/files.php +++ b/app/controllers/course/files.php @@ -195,6 +195,7 @@ class Course_FilesController extends AuthenticatedController $this->range_type = 'course'; $this->show_default_sidebar = true; $this->form_action = $this->url_for('file/bulk/' . $folder->getId()); + $this->enable_table_filter = true; $this->render_template('files/flat.php', $this->layout); } diff --git a/app/controllers/institute/files.php b/app/controllers/institute/files.php index e7a21d13a63a4f07c4132e5057de2f364d203c5d..0bacb0e4e3d5bad62551c81407e2e7224a99eaf4 100644 --- a/app/controllers/institute/files.php +++ b/app/controllers/institute/files.php @@ -148,6 +148,7 @@ class Institute_FilesController extends AuthenticatedController $this->range_type = 'institute'; $this->show_default_sidebar = true; + $this->enable_table_filter = true; $this->form_action = $this->url_for('file/bulk/' . $folder->getId()); $this->render_template('files/flat.php', $this->layout); } diff --git a/app/views/files/flat.php b/app/views/files/flat.php index 77027678c11a81bfd1e45a05c4cb7cdabfa4c36e..4c4c74c8fb42299f7bd23460b7a81f87061ab054 100644 --- a/app/views/files/flat.php +++ b/app/views/files/flat.php @@ -12,7 +12,6 @@ $topFolder = new StandardFolder(); $vue_topFolder = [ 'description' => $topFolder->getDescriptionTemplate(), 'additionalColumns' => $topFolder->getAdditionalColumns(), - 'buttons' => null ]; if (is_a($vue_topFolder['description'], "Flexi_Template")) { $vue_topFolder['description'] = $vue_topFolder['description']->render(); @@ -55,29 +54,20 @@ foreach ($topFolder->getAdditionalActionButtons() as $button) { :files="files" :folders="folders" :topfolder="topfolder" - enable_table_filter="<?= $enable_table_filter ? 'true' : 'false' ?>" + :allow_filter="<?= json_encode(!empty($enable_table_filter)) ?>" table_title="<?= htmlReady($table_title) ?>" pagination="<?= htmlReady($pagination_html) ?>" :initial_sort="{sortedBy:'chdate',sortDirection:'desc'}" ></files-table> </form> - -<? ob_start(); ?> -<? if ($enable_table_filter) : ?> -<div align="center"> -<input class="tablesorterfilter" placeholder="<?= _('Name oder Autor/-in') ?>" data-column="2,4" type="search" style="width: 100%; margin-bottom: 5px;"><br> -</div> -<? endif ?> <? if ($show_default_sidebar) { - if ($enable_table_filter) { - $content = ob_get_clean(); + if (!empty($enable_table_filter)) { $widget = new SidebarWidget(); + $widget->setId('table-view-filter'); $widget->setTitle(_('Filter')); - $widget->addElement(new WidgetElement($content)); + $widget->addElement(new WidgetElement('<div></div>')); Sidebar::get()->addWidget($widget); - } else { - ob_get_clean(); } $views = new ViewsWidget(); @@ -96,6 +86,4 @@ if ($show_default_sidebar) { 'flat' )->setActive(true); Sidebar::get()->addWidget($views); -} else { - ob_get_clean(); } diff --git a/lib/filesystem/FilesystemVueDataManager.php b/lib/filesystem/FilesystemVueDataManager.php index b0f69285fec05e3de3a1152a9f4a2dcb70564376..87769e80071f4c10f716dc33f28147b4a05f6981 100644 --- a/lib/filesystem/FilesystemVueDataManager.php +++ b/lib/filesystem/FilesystemVueDataManager.php @@ -35,7 +35,7 @@ class FilesystemVueDataManager 'icon' => $file->getIcon($isDownloadable ? Icon::ROLE_CLICKABLE : Icon::ROLE_INFO)->getShape(), 'size' => $file->getSize(), 'author_url' => $file->getUser() && $file->getUserId() !== $GLOBALS['user']->id ? URLHelper::getURL('dispatch.php/profile', ['username' => $file->getUser()->username], true) : "", - 'author_name' => $file->getUserName(), + 'author_name' => $file->getUserName() ?: '', 'author_id' => $file->getUserId(), 'chdate' => (int) $file->getLastChangeDate(), 'additionalColumns' => $additionalColumns, diff --git a/package-lock.json b/package-lock.json index 5fa6fc0394961408642b4987299f767fd4fb8f69..549b66e697ca12bfde6e888d0ba3a8de0be00a6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -61,6 +61,7 @@ "portal-vue": "^2.1.7", "postcss": "^8.1.8", "postcss-loader": "4.1.0", + "sanitize-html": "^2.7.0", "sass": "^1.29.0", "sass-loader": "^10.1.0", "select2": "4.0.13", @@ -4049,6 +4050,15 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -8153,9 +8163,9 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true, "bin": { "nanoid": "bin/nanoid.cjs" @@ -8661,6 +8671,12 @@ "node": ">=6" } }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=", + "dev": true + }, "node_modules/parse5": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", @@ -8766,6 +8782,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -8868,21 +8890,27 @@ } }, "node_modules/postcss": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", - "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], "dependencies": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" }, "engines": { "node": "^10 || ^12 || >=14" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" } }, "node_modules/postcss-calc": { @@ -11333,6 +11361,41 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sanitize-html": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", + "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", + "dev": true, + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sanitize-html/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sanitize-html/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/sass": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.29.0.tgz", @@ -11958,9 +12021,9 @@ } }, "node_modules/source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true, "engines": { "node": ">=0.10.0" @@ -17673,6 +17736,12 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -20939,9 +21008,9 @@ "optional": true }, "nanoid": { - "version": "3.1.23", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.23.tgz", - "integrity": "sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", "dev": true }, "nanomatch": { @@ -21332,6 +21401,12 @@ } } }, + "parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=", + "dev": true + }, "parse5": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-1.5.1.tgz", @@ -21422,6 +21497,12 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -21503,14 +21584,14 @@ "dev": true }, "postcss": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", - "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", + "version": "8.4.13", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.13.tgz", + "integrity": "sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==", "dev": true, "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" + "nanoid": "^3.3.3", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" } }, "postcss-calc": { @@ -23529,6 +23610,34 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "sanitize-html": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.7.0.tgz", + "integrity": "sha512-jfQelabOn5voO7FAfnQF7v+jsA6z9zC/O4ec0z3E35XPEtHYJT/OdUziVWlKW4irCr2kXaQAyXTXDHWAibg1tA==", + "dev": true, + "requires": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^6.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + }, + "dependencies": { + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true + } + } + }, "sass": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/sass/-/sass-1.29.0.tgz", @@ -24065,9 +24174,9 @@ "dev": true }, "source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", "dev": true }, "source-map-resolve": { diff --git a/package.json b/package.json index 6f883cd72b5d590905f9be825f9bd8f8da8f491a..6fd5bdede217da36fd0b49b331dfe9ca7da0eb7d 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "portal-vue": "^2.1.7", "postcss": "^8.1.8", "postcss-loader": "4.1.0", + "sanitize-html": "^2.7.0", "sass": "^1.29.0", "sass-loader": "^10.1.0", "select2": "4.0.13", diff --git a/resources/assets/javascripts/bootstrap/files.js b/resources/assets/javascripts/bootstrap/files.js index 7118e083db0a04c8ddeadaa77923d29d29be3ede..310b2ed08947e5eb56fd85450439f146570b61a2 100644 --- a/resources/assets/javascripts/bootstrap/files.js +++ b/resources/assets/javascripts/bootstrap/files.js @@ -34,22 +34,6 @@ STUDIP.domReady(() => { $('.file_selector input[type=file]').first().click(); }); - $('table.documents.flat.filter').each(function () { - var ignored = []; - $('colgroup col', this).each((index, col) => { - if ($(col).is('[data-filter-ignore]')) { - ignored.push(index); - } - }); - $(this).filterTable({ - highlightClass: 'filter-match', - ignoreColumns: ignored, - inputSelector: '.sidebar .tablesorterfilter', - minChars: 1, - minRows: 1 - }); - }); - $(document).trigger('refresh-handlers'); $(document).on( diff --git a/resources/assets/javascripts/jquery-bundle.js b/resources/assets/javascripts/jquery-bundle.js index 80ec6cf46c60ed44a4e455deca2b6c38feef9268..51e3b550b6887fb98b61ab74a45df72e2558a40c 100644 --- a/resources/assets/javascripts/jquery-bundle.js +++ b/resources/assets/javascripts/jquery-bundle.js @@ -75,7 +75,6 @@ import 'sticky-kit/dist/sticky-kit.js'; import 'blueimp-file-upload'; import 'blueimp-file-upload/js/jquery.iframe-transport.js'; -import './jquery/jquery.filtertable-1.5.7.js'; import './jquery/autoresize.jquery.min.js'; import { $gettext } from './lib/gettext.js'; diff --git a/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js b/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js deleted file mode 100644 index e65af7f14b785b2a6d055e3fc3e426ffa01f4627..0000000000000000000000000000000000000000 --- a/resources/assets/javascripts/jquery/jquery.filtertable-1.5.7.js +++ /dev/null @@ -1,402 +0,0 @@ -/** - * jquery.filterTable - * - * This plugin will add a search filter to tables. When typing in the filter, - * any rows that do not contain the filter will be hidden. - * - * Utilizes bindWithDelay() if available. https://github.com/bgrins/bindWithDelay - * - * @version v1.5.7 - * @author Sunny Walker, swalker@hawaii.edu - * @license MIT - */ -(function ($) { - var jversion = $.fn.jquery.split('.'), - jmajor = parseFloat(jversion[0]), - jminor = parseFloat(jversion[1]); - // build the pseudo selector for jQuery < 1.8 - if (jmajor < 2 && jminor < 8) { - // build the case insensitive filtering functionality as a pseudo-selector expression - $.expr[':'].filterTableFind = function (a, i, m) { - return $(a).text().toUpperCase().indexOf(m[3].toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0; - }; - // build the case insensitive all-words filtering functionality as a pseudo-selector expression - $.expr[':'].filterTableFindAny = function (a, i, m) { - // build an array of each non-falsey value passed - var raw_args = m[3].split(/[\s,]/), - args = []; - $.each(raw_args, function (j, v) { - var t = v.replace(/^\s+|\s$/g, ''); - if (t) { - args.push(t); - } - }); - // if there aren't any non-falsey values to search for, abort - if (!args.length) { - return false; - } - return function (a) { - var found = false; - $.each(args, function (j, v) { - if ($(a).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) { - found = true; - return false; - } - }); - return found; - }; - }; - // build the case insensitive all-words filtering functionality as a pseudo-selector expression - $.expr[':'].filterTableFindAll = function (a, i, m) { - // build an array of each non-falsey value passed - var raw_args = m[3].split(/[\s,]/), - args = []; - $.each(raw_args, function (j, v) { - var t = v.replace(/^\s+|\s$/g, ''); - if (t) { - args.push(t); - } - }); - // if there aren't any non-falsey values to search for, abort - if (!args.length) { - return false; - } - return function (a) { - // how many terms were found? - var found = 0; - $.each(args, function (j, v) { - if ($(a).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) { - // found another term - found++; - } - }); - return found === args.length; // did we find all of them in this cell? - }; - }; - } else { - // build the pseudo selector for jQuery >= 1.8 - $.expr[':'].filterTableFind = jQuery.expr.createPseudo(function (arg) { - return function (el) { - return $(el).text().toUpperCase().indexOf(arg.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0; - }; - }); - $.expr[':'].filterTableFindAny = jQuery.expr.createPseudo(function (arg) { - // build an array of each non-falsey value passed - var raw_args = arg.split(/[\s,]/), - args = []; - $.each(raw_args, function (i, v) { - // trim the string - var t = v.replace(/^\s+|\s$/g, ''); - if (t) { - args.push(t); - } - }); - // if there aren't any non-falsey values to search for, abort - if (!args.length) { - return false; - } - return function (el) { - var found = false; - $.each(args, function (i, v) { - if ($(el).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) { - found = true; - // short-circuit the searching since this cell has one of the terms - return false; - } - }); - return found; - }; - }); - $.expr[':'].filterTableFindAll = jQuery.expr.createPseudo(function (arg) { - // build an array of each non-falsey value passed - var raw_args = arg.split(/[\s,]/), - args = []; - $.each(raw_args, function (i, v) { - // trim the string - var t = v.replace(/^\s+|\s$/g, ''); - if (t) { - args.push(t); - } - }); - // if there aren't any non-falsey values to search for, abort - if (!args.length) { - return false; - } - return function (el) { - // how many terms were found? - var found = 0; - $.each(args, function (i, v) { - if ($(el).text().toUpperCase().indexOf(v.toUpperCase().replace(/"""/g, '"').replace(/"\\"/g, "\\")) >= 0) { - // found another term - found++; - } - }); - // did we find all of them in this cell? - return found === args.length; - }; - }); - } - // define the filterTable plugin - $.fn.filterTable = function (options) { - // start off with some default settings - var defaults = { - // make the filter input field autofocused (not recommended for accessibility) - autofocus: false, - - // callback function: function (term, table){} - callback: null, - - // class to apply to the container - containerClass: 'filter-table', - - // tag name of the container - containerTag: 'p', - - // jQuery expression method to use for filtering - filterExpression: 'filterTableFind', - - // if true, the table's tfoot(s) will be hidden when the table is filtered - hideTFootOnFilter: false, - - // class applied to cells containing the filter term - highlightClass: 'alt', - - // don't filter the contents of cells with this class - ignoreClass: '', - - // don't filter the contents of these columns - ignoreColumns: [], - - // use the element with this selector for the filter input field instead of creating one - inputSelector: null, - - // name of filter input field - inputName: '', - - // tag name of the filter input tag - inputType: 'search', - - // text to precede the filter input tag - label: 'Filter:', - - // filter only when at least this number of characters are in the filter input field - minChars: 1, - - // don't show the filter on tables with at least this number of rows - minRows: 8, - - // HTML5 placeholder text for the filter field - placeholder: 'search this table', - - // prevent the return key in the filter input field from trigger form submits - preventReturnKey: true, - - // list of phrases to quick fill the search - quickList: [], - - // class of each quick list item - quickListClass: 'quick', - - // quick list item label to clear the filter (e.g., '× Clear filter') - quickListClear: '', - - // tag surrounding quick list items (e.g., ul) - quickListGroupTag: '', - - // tag type of each quick list item (e.g., a or li) - quickListTag: 'a', - - // class applied to visible rows - visibleClass: 'visible' - }, - // mimic PHP's htmlspecialchars() function - hsc = function (text) { - return text.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>'); - }, - // merge the user's settings into the defaults - settings = $.extend({}, defaults, options); - - // handle the actual table filtering - var doFiltering = function (table, q) { - // cache the tbody element - var tbody = table.find('tbody'); - // if the filtering query is blank or the number of chars is less than the minChars option - if (q === '' || q.length < settings.minChars) { - // show all rows - tbody.find('tr').show().addClass(settings.visibleClass); - // remove the row highlight from all cells - tbody.find('td').removeClass(settings.highlightClass); - // show footer if the setting was specified - if (settings.hideTFootOnFilter) { - table.find('tfoot').show(); - } - } else { - // if the filter query is not blank - var all_tds = tbody.find('td'); - // hide all rows, assuming none were found - tbody.find('tr').hide().removeClass(settings.visibleClass); - // remove previous highlights - all_tds.removeClass(settings.highlightClass); - // hide footer if the setting was specified - if (settings.hideTFootOnFilter) { - table.find('tfoot').hide(); - } - if (settings.ignoreColumns.length) { - var tds = []; - if (settings.ignoreClass) { - all_tds = all_tds.not('.' + settings.ignoreClass); - } - tds = all_tds.filter(':' + settings.filterExpression + '("' + q + '")'); - tds.each(function () { - var t = $(this), - col = t.parent().children().index(t); - if ($.inArray(col, settings.ignoreColumns) === -1) { - t.addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass); - } - }); - } else { - if (settings.ignoreClass) { - all_tds = all_tds.not('.' + settings.ignoreClass); - } - // highlight (class=alt) only the cells that match the query and show their rows - all_tds.filter(':' + settings.filterExpression + '("' + q + '")').addClass(settings.highlightClass).closest('tr').show().addClass(settings.visibleClass); - } - } - // call the callback function - if (settings.callback) { - settings.callback(q, table); - } - }; // doFiltering() - - return this.each(function () { - // cache the table - var t = $(this), - // cache the tbody - tbody = t.find('tbody'), - // placeholder for the filter field container DOM node - container = null, - // placeholder for the quick list items - quicks = null, - // placeholder for the field field DOM node - filter = null, - // was the filter created or chosen from an existing element? - created_filter = true; - - // only if object is a table and there's a tbody and at least minRows trs and hasn't already had a filter added - if (t[0].nodeName === 'TABLE' && tbody.length > 0 && (settings.minRows === 0 || (settings.minRows > 0 && tbody.find('tr').length >= settings.minRows)) && !t.prev().hasClass(settings.containerClass)) { - // use a single existing field as the filter input field - if (settings.inputSelector && $(settings.inputSelector).length === 1) { - filter = $(settings.inputSelector); - // container to hold the quick list options - container = filter.parent(); - created_filter = false; - } else { - // create the filter input field (and container) - // build the container tag for the filter field - container = $('<' + settings.containerTag + ' />'); - // add any classes that need to be added - if (settings.containerClass !== '') { - container.addClass(settings.containerClass); - } - // add the label for the filter field - container.prepend(settings.label + ' '); - // build the filter field - filter = $('<input type="' + settings.inputType + '" placeholder="' + settings.placeholder + '" name="' + settings.inputName + '" />'); - // prevent return in the filter field from submitting any forms - if (settings.preventReturnKey) { - filter.on('keydown', function (ev) { - if ((ev.keyCode || ev.which) === 13) { - ev.preventDefault(); - return false; - } - }); - } - } - - // add the autofocus attribute if requested - if (settings.autofocus) { - filter.attr('autofocus', true); - } - - // does bindWithDelay() exist? - if ($.fn.bindWithDelay) { - // bind doFiltering() to keyup (delayed) - filter.bindWithDelay('keyup', function () { - doFiltering(t, $(this).val()); - }, 200); - } else { - // just bind to onKeyUp - // bind doFiltering() to keyup - filter.bind('keyup', function () { - doFiltering(t, $(this).val()); - }); - } - - // bind doFiltering() to additional events - filter.bind('click search input paste blur', function () { - doFiltering(t, $(this).val()); - }); - - // add the filter field to the container if it was created by the plugin - if (created_filter) { - container.append(filter); - } - - // are there any quick list items to add? - if (settings.quickList.length > 0 || settings.quickListClear) { - quicks = settings.quickListGroupTag ? $('<' + settings.quickListGroupTag + ' />') : container; - // for each quick list item... - $.each(settings.quickList, function (index, value) { - // build the quick list item link - var q = $('<' + settings.quickListTag + ' class="' + settings.quickListClass + '" />'); - // add the item's text - q.text(hsc(value)); - if (q[0].nodeName === 'A') { - // add a (worthless) href to the item if it's an anchor tag so that it gets the browser's link treatment - q.attr('href', '#'); - } - // bind the click event to it - q.bind('click', function (e) { - // stop the normal anchor tag behavior from happening - e.preventDefault(); - // send the quick list value over to the filter field and trigger the event - filter.val(value).focus().trigger('click'); - }); - // add the quick list link to the quick list groups container - quicks.append(q); - }); - - // add the quick list clear item if a label has been specified - if (settings.quickListClear) { - // build the clear item - var q = $('<' + settings.quickListTag + ' class="' + settings.quickListClass + '" />'); - // add the label text - q.html(settings.quickListClear); - if (q[0].nodeName === 'A') { - // add a (worthless) href to the item if it's an anchor tag so that it gets the browser's link treatment - q.attr('href', '#'); - } - // bind the click event to it - q.bind('click', function (e) { - e.preventDefault(); - // clear the quick list value and trigger the event - filter.val('').focus().trigger('click'); - }); - // add the clear item to the quick list groups container - quicks.append(q); - } - - // add the quick list groups container to the DOM if it isn't already there - if (quicks !== container) { - container.append(quicks); - } - } - - // add the filter field and quick list container to just before the table if it was created by the plugin - if (created_filter) { - t.before(container); - } - } - }); // return this.each - }; // $.fn.filterTable -})(jQuery); diff --git a/resources/assets/stylesheets/less/files.less b/resources/assets/stylesheets/less/files.less index 826b5929b7b36c2914f8d2e8e977512e0943e6e1..5d2fbc83a1be0a86b75ab6ca504af59954c231f3 100644 --- a/resources/assets/stylesheets/less/files.less +++ b/resources/assets/stylesheets/less/files.less @@ -444,10 +444,6 @@ table.documents { } } - &.flat td.filter-match { - background-color: @yellow-20; - } - tr:target { background-color: @activity-color-20; } diff --git a/resources/vue/components/FilesTable.vue b/resources/vue/components/FilesTable.vue index 4587265a756b9b68498a9c29f12170719ee9fea6..6d6eff3dc49eb31561eb25ba9a08e00a1168064f 100644 --- a/resources/vue/components/FilesTable.vue +++ b/resources/vue/components/FilesTable.vue @@ -1,227 +1,242 @@ <template> - <table class="default documents" - :data-folder_id="topfolder.folder_id" - data-shiftcheck> - <caption> - <div class="caption-container"> - <div v-if="breadcrumbs && !table_title"> - <a v-if="breadcrumbs[0]" :href="breadcrumbs[0].url" :title="$gettext('Zum Hauptordner')"> - <studip-icon shape="folder-home-full" - role="clickable" - class="text-bottom" - size="30"></studip-icon> - <span v-if="breadcrumbs.length == 1"> - {{ breadcrumbs[0].name }} - </span> - </a> - <span v-for="(breadcrumb, index) in breadcrumbs" - :key="breadcrumb.folder_id" - v-if="index > 0"> - /<a :href="breadcrumb.url"> - {{breadcrumb.name}} + <div> + <table class="default documents" + :data-folder_id="topfolder.folder_id" + data-shiftcheck> + <caption> + <div class="caption-container"> + <div v-if="breadcrumbs && !table_title"> + <a v-if="breadcrumbs[0]" :href="breadcrumbs[0].url" :title="$gettext('Zum Hauptordner')"> + <studip-icon shape="folder-home-full" + role="clickable" + class="text-bottom" + size="30"></studip-icon> + <span v-if="breadcrumbs.length === 1"> + {{ breadcrumbs[0].name }} + </span> </a> - </span> + <span v-for="(breadcrumb, index) in breadcrumbs" + :key="breadcrumb.folder_id" + v-if="index > 0"> + /<a :href="breadcrumb.url"> + {{ breadcrumb.name }} + </a> + </span> + </div> + <div v-if="table_title">{{table_title}}</div> </div> - <div v-if="table_title">{{table_title}}</div> - </div> - <div v-if="topfolder.description" style="font-size: small" v-html="topfolder.description"></div> - </caption> + <div v-if="topfolder.description" style="font-size: small" v-html="topfolder.description"></div> + </caption> - <colgroup> - <col v-if="show_bulk_actions" width="30px" data-filter-ignore> - <col width="60px" data-filter-ignore> - <col> - <col width="100px" class="responsive-hidden" data-filter-ignore> - <col v-if="showdownloads" width="100px" class="responsive-hidden" data-filter-ignore> - <col width="150px" class="responsive-hidden"> - <col width="120px" class="responsive-hidden" data-filter-ignore> - <col v-if="topfolder.additionalColumns" - v-for="(name, index) in topfolder.additionalColumns" - :key="index" - data-filter-ignore - class="responsive-hidden"> - <col width="80px" data-filter-ignore> - </colgroup> - <thead> - <tr class="sortable"> - <th v-if="show_bulk_actions" data-sort="false"> - <studip-proxy-checkbox - v-model="selectedIds" - :total="allIds" - ></studip-proxy-checkbox> - </th> - <th @click="sort('mime_type')" :class="sortClasses('mime_type')"> - <a href="#" @click.prevent> - {{ $gettext('Typ') }} - </a> - </th> - <th @click="sort('name')" :class="sortClasses('name')"> - <a href="#" @click.prevent> - {{ $gettext('Name') }} - </a> - </th> - <th @click="sort('size')" class="responsive-hidden" :class="sortClasses('size')"> - <a href="#" @click.prevent> - {{ $gettext('Größe') }} - </a> - </th> - <th v-if="showdownloads" @click="sort('downloads')" class="responsive-hidden" :class="sortClasses('downloads')"> - <a href="#" @click.prevent> - {{ $gettext('Downloads') }} - </a> - </th> - <th class="responsive-hidden" @click="sort('author_name')" :class="sortClasses('author_name')"> - <a href="#" @click.prevent> - {{ $gettext('Autor/-in') }} - </a> - </th> - <th class="responsive-hidden" @click="sort('chdate')" :class="sortClasses('chdate')"> - <a href="#" @click.prevent> - {{ $gettext('Datum') }} - </a> - </th> - <th v-if="topfolder.additionalColumns" - v-for="(name, index) in topfolder.additionalColumns" - :key="index" - @click="sort(index)" - class="responsive-hidden" - :class="sortClasses(index)"> - <a href="#" @click.prevent> - {{name}} - </a> - - </th> - <th data-sort="false">{{ $gettext('Aktionen') }}</th> - </tr> - </thead> - <tbody class="subfolders"> - <tr v-if="files.length + folders.length == 0" class="empty"> - <td :colspan="numberOfColumns"> - {{ $gettext('Dieser Ordner ist leer') }} - </td> - </tr> - <tr v-for="folder in sortedFolders" - :id="'row_folder_' + folder.id " - :data-permissions="folder.permissions"> - <td v-if="show_bulk_actions"> - <studip-proxied-checkbox - name="ids[]" - :value="folder.id" - v-model="selectedIds" - ></studip-proxied-checkbox> - </td> - <td class="document-icon"> - <a :href="folder.url"> - <studip-icon :shape="folder.icon" role="clickable" size="26" class="text-bottom"></studip-icon> - </a> - </td> - <td> - <a :href="folder.url">{{folder.name}}</a> - </td> - <td class="responsive-hidden"></td> - <td v-if="showdownloads" - class="responsive-hidden"> - </td> - <td v-if="folder.author_url" class="responsive-hidden"> - <a :href="folder.author_url">{{folder.author_name}}</a> - </td> - <td v-else class="responsive-hidden"> - {{folder.author_name}} - </td> - <td class="responsive-hidden" style="white-space: nowrap;"> - <studip-date-time :timestamp="folder.chdate" :relative="true"></studip-date-time> - </td> - <template v-if="topfolder.additionalColumns" - v-for="(name, index) in topfolder.additionalColumns"> - <td v-if="folder.additionalColumns && folder.additionalColumns[index] && folder.additionalColumns[index].html" + <colgroup> + <col v-if="show_bulk_actions" style="width: 30px"> + <col style="width: 60px"> + <col> + <col style="width: 100px" class="responsive-hidden"> + <col v-if="showdownloads" style="width: 100px" class="responsive-hidden"> + <col style="width: 150px" class="responsive-hidden"> + <col style="width: 120px" class="responsive-hidden"> + <col v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns" + :key="index" + data-filter-ignore + class="responsive-hidden"> + <col style="width: 80px"> + </colgroup> + <thead> + <tr class="sortable"> + <th v-if="show_bulk_actions" data-sort="false"> + <studip-proxy-checkbox + v-model="selectedIds" + :total="allIds" + ></studip-proxy-checkbox> + </th> + <th @click="sort('mime_type')" :class="sortClasses('mime_type')"> + <a href="#" @click.prevent> + {{ $gettext('Typ') }} + </a> + </th> + <th @click="sort('name')" :class="sortClasses('name')"> + <a href="#" @click.prevent> + {{ $gettext('Name') }} + </a> + </th> + <th @click="sort('size')" class="responsive-hidden" :class="sortClasses('size')"> + <a href="#" @click.prevent> + {{ $gettext('Größe') }} + </a> + </th> + <th v-if="showdownloads" @click="sort('downloads')" class="responsive-hidden" :class="sortClasses('downloads')"> + <a href="#" @click.prevent> + {{ $gettext('Downloads') }} + </a> + </th> + <th class="responsive-hidden" @click="sort('author_name')" :class="sortClasses('author_name')"> + <a href="#" @click.prevent> + {{ $gettext('Autor/-in') }} + </a> + </th> + <th class="responsive-hidden" @click="sort('chdate')" :class="sortClasses('chdate')"> + <a href="#" @click.prevent> + {{ $gettext('Datum') }} + </a> + </th> + <th v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns" + :key="index" + @click="sort(index)" class="responsive-hidden" - v-html="folder.additionalColumns[index].html"></td> - <td v-else class="responsive-hidden"></td> - </template> - <td class="actions" v-html="folder.actions"> - </td> - </tr> - </tbody> - <tbody class="files"> - <tr v-for="file in sortedFiles" - :class="file.new ? 'new' : ''" - :id="'fileref_' + file.id" - role="row" - :data-permissions="getPermissions(file)"> - <td v-if="show_bulk_actions"> - <studip-proxied-checkbox - name="ids[]" - :value="file.id" - v-model="selectedIds" - ></studip-proxied-checkbox> - </td> - <td class="document-icon"> - <a v-if="file.download_url" :href="file.download_url" target="_blank" rel="noopener noreferrer"> - <studip-icon :shape="file.icon" role="clickable" size="24" class="text-bottom"></studip-icon> - </a> - <studip-icon v-else :shape="file.icon" role="clickable" size="24"></studip-icon> + :class="sortClasses(index)"> + <a href="#" @click.prevent> + {{name}} + </a> - <a :href="file.download_url" - v-if="file.download_url && file.mime_type.indexOf('image/') === 0" - class="lightbox-image" - data-lightbox="gallery"></a> - </td> - <td> - <a :href="file.details_url" data-dialog>{{file.name}}</a> + </th> + <th data-sort="false">{{ $gettext('Aktionen') }}</th> + </tr> + </thead> + <tbody class="subfolders"> + <tr v-if="!hasData" class="empty"> + <td :colspan="numberOfColumns"> + {{ $gettext('Dieser Ordner ist leer') }} + </td> + </tr> + <tr v-else-if="displayedFolders.length + displayedFiles.length === 0" class="empty"> + <td :colspan="numberOfColumns"> + <translate>Keine Ordner oder Dateien entsprechen Ihrem Filter.</translate> + </td> + </tr> + <tr v-for="folder in displayedFolders" + :id="'row_folder_' + folder.id " + :data-permissions="folder.permissions"> + <td v-if="show_bulk_actions"> + <studip-proxied-checkbox + name="ids[]" + :value="folder.id" + v-model="selectedIds" + ></studip-proxied-checkbox> + </td> + <td class="document-icon"> + <a :href="folder.url"> + <studip-icon :shape="folder.icon" role="clickable" size="26" class="text-bottom"></studip-icon> + </a> + </td> + <td :class="{'filter-match': valueMatchesFilter(folder.name)}"> + <a :href="folder.url"> + <span v-html="highlightString(folder.name)"></span> + </a> + </td> + <td class="responsive-hidden"></td> + <td v-if="showdownloads" + class="responsive-hidden"> + </td> + <td class="responsive-hidden" :class="{'filter-match': valueMatchesFilter(folder.author_name)}"> + <a v-if="folder.author_url" :href="folder.author_url"> + <span v-html="highlightString(folder.author_name)"></span> + </a> + <span v-else v-html="highlightString(folder.author_name)"></span> + </td> + <td class="responsive-hidden" style="white-space: nowrap;"> + <studip-date-time :timestamp="folder.chdate" :relative="true"></studip-date-time> + </td> + <template v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns"> + <td v-if="folder.additionalColumns && folder.additionalColumns[index] && folder.additionalColumns[index].html" + class="responsive-hidden" + v-html="folder.additionalColumns[index].html"></td> + <td v-else class="responsive-hidden"></td> + </template> + <td class="actions" v-html="folder.actions"> + </td> + </tr> + </tbody> + <tbody class="files"> + <tr v-for="file in displayedFiles" + :class="file.new ? 'new' : ''" + :id="'fileref_' + file.id" + role="row" + :data-permissions="getPermissions(file)"> + <td v-if="show_bulk_actions"> + <studip-proxied-checkbox + name="ids[]" + :value="file.id" + v-model="selectedIds" + ></studip-proxied-checkbox> + </td> + <td class="document-icon"> + <a v-if="file.download_url" :href="file.download_url" target="_blank" rel="noopener noreferrer"> + <studip-icon :shape="file.icon" role="clickable" size="24" class="text-bottom"></studip-icon> + </a> + <studip-icon v-else :shape="file.icon" role="clickable" size="24"></studip-icon> - <studip-icon v-if="file.restrictedTermsOfUse" - shape="lock-locked" - role="info" - size="16" - :title="$gettext('Das Herunterladen dieser Datei ist nur eingeschränkt möglich.')"></studip-icon> - </td> - <td :data-sort-value="file.size" - class="responsive-hidden"> - <studip-file-size v-if="file.size !== null" :size="parseInt(file.size, 10)"></studip-file-size> - </td> - <td v-if="showdownloads" - class="responsive-hidden"> - {{file.downloads}} - </td> - <td v-if="file.author_url" class="responsive-hidden" > - <a :href="file.author_url"> - {{file.author_name}} - </a> - </td> - <td v-else class="responsive-hidden"> - {{file.author_name}} - </td> - <td data-sort-value="file.chdate" class="responsive-hidden" style="white-space: nowrap;"> - <studip-date-time :timestamp="file.chdate" :relative="true"></studip-date-time> - </td> - <template v-if="topfolder.additionalColumns" - v-for="(name, index) in topfolder.additionalColumns"> - <td v-if="file.additionalColumns && file.additionalColumns[index] && file.additionalColumns[index].html" - class="responsive-hidden" - v-html="file.additionalColumns[index].html"></td> - <td v-else class="responsive-hidden"></td> - </template> - <td class="actions" v-html="file.actions"> - </td> - </tr> - </tbody> - <tfoot v-if="(topfolder.buttons && show_bulk_actions) || tfoot_link"> - <tr> - <td :colspan="numberOfColumns - (tfoot_link ? 1 : 0)"> - <div class="footer-items"> - <span v-if="topfolder.buttons && show_bulk_actions" - v-html="topfolder.buttons" class="bulk-buttons" ref="buttons"></span> - <span v-if="tfoot_link" :colspan="(topfolder.buttons && show_bulk_actions ? 1 : numberOfColumns)"> - <a :href="tfoot_link.href">{{tfoot_link.text}}</a> - </span> - <span v-if="pagination" v-html="pagination" class="pagination"></span> - </div> - </td> - </tr> - </tfoot> - </table> + <a :href="file.download_url" + v-if="file.download_url && file.mime_type.indexOf('image/') === 0" + class="lightbox-image" + data-lightbox="gallery"></a> + </td> + <td :class="{'filter-match': valueMatchesFilter(file.name)}"> + <a :href="file.details_url" data-dialog> + <span v-html="highlightString(file.name)"></span> + </a> + + <studip-icon v-if="file.restrictedTermsOfUse" + shape="lock-locked" + role="info" + size="16" + :title="$gettext('Das Herunterladen dieser Datei ist nur eingeschränkt möglich.')"></studip-icon> + </td> + <td :data-sort-value="file.size" + class="responsive-hidden"> + <studip-file-size v-if="file.size !== null" :size="parseInt(file.size, 10)"></studip-file-size> + </td> + <td v-if="showdownloads" + class="responsive-hidden"> + {{file.downloads}} + </td> + <td class="responsive-hidden" :class="{'filter-match': valueMatchesFilter(file.author_name)}"> + <a v-if="file.author_url" :href="file.author_url"> + <span v-html="highlightString(file.author_name)"></span> + </a> + <span v-else v-html="highlightString(file.author_name)"></span> + </td> + <td data-sort-value="file.chdate" class="responsive-hidden" style="white-space: nowrap;"> + <studip-date-time :timestamp="file.chdate" :relative="true"></studip-date-time> + </td> + <template v-if="topfolder.additionalColumns" + v-for="(name, index) in topfolder.additionalColumns"> + <td v-if="file.additionalColumns && file.additionalColumns[index] && file.additionalColumns[index].html" + class="responsive-hidden" + v-html="file.additionalColumns[index].html"></td> + <td v-else class="responsive-hidden"></td> + </template> + <td class="actions" v-html="file.actions"> + </td> + </tr> + </tbody> + <tfoot v-if="(topfolder.buttons && show_bulk_actions) || tfoot_link"> + <tr> + <td :colspan="numberOfColumns - (tfoot_link ? 1 : 0)"> + <div class="footer-items"> + <span v-if="topfolder.buttons && show_bulk_actions" + v-html="topfolder.buttons" class="bulk-buttons" ref="buttons"></span> + <span v-if="tfoot_link" :colspan="(topfolder.buttons && show_bulk_actions ? 1 : numberOfColumns)"> + <a :href="tfoot_link.href">{{tfoot_link.text}}</a> + </span> + <span v-if="pagination" v-html="pagination" class="pagination"></span> + </div> + </td> + </tr> + </tfoot> + </table> + + <MountingPortal v-if="allow_filter" mount-to="#table-view-filter .sidebar-widget-content div" name="sidebar-content-toggle"> + <input :placeholder="$gettext('Name oder Autor/in')" type="search" v-model="filter" :disabled="!hasData" /> + </MountingPortal> + </div> </template> <script> +import sanitizeHTML from 'sanitize-html'; + export default { name: 'files-table', props: { @@ -266,13 +281,18 @@ export default { type: Object, required: false, default: () => ({sortedBy: 'name', sortDirection: 'asc'}) + }, + allow_filter: { + type: Boolean, + default: false } }, data () { return { selectedIds: [undefined], // Includes invalid value to trigger watch on mounted sortedBy: this.initial_sort.sortedBy, - sortDirection: this.initial_sort.sortDirection + sortDirection: this.initial_sort.sortDirection, + filter: '' }; }, methods: { @@ -341,6 +361,22 @@ export default { permissions += 'w'; } return permissions; + }, + valueMatchesFilter(string) { + if (this.needleForFilter.length === 0) { + return false; + } + return string.toLowerCase().includes(this.needleForFilter); + }, + highlightString (string) { + let highlighted = sanitizeHTML(string); + if (this.needleForFilter.length > 0) { + // Escape needle for regexp, see https://stackoverflow.com/a/3561711 + const pattern = this.needleForFilter.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + const regExp = new RegExp(pattern, 'gi'); + highlighted = highlighted.replace(regExp, '<span class="filter-match">$&</span>'); + } + return highlighted; } }, computed: { @@ -357,6 +393,32 @@ export default { }, allIds () { return [].concat(this.files.map(file => file.id)).concat(this.folders.map(folder => folder.id)); + }, + displayedFiles () { + let files = [].concat(this.files); + if (this.needleForFilter.length > 0) { + files = files.filter(file => { + return this.valueMatchesFilter(file.name) + || this.valueMatchesFilter(file.author_name); + }); + } + return this.sortArray(files); + }, + displayedFolders () { + let folders = [].concat(this.folders); + if (this.needleForFilter.length > 0) { + folders = folders.filter(folder => { + return this.valueMatchesFilter(folder.name) + || this.valueMatchesFilter(folder.author_name); + }); + } + return this.sortArray(folders); + }, + needleForFilter () { + return this.filter.trim().toLowerCase(); + }, + hasData () { + return this.folders.length + this.files.length > 0; } }, mounted () { @@ -385,3 +447,19 @@ export default { } } </script> +<style lang="scss"> +#table-view-filter { + input[type="search"] { + width: 100%; + } +} +table.documents { + td.filter-match { + background-color: var(--yellow-20); + } + span.filter-match { + font-weight: bold; + text-decoration: underline; + } +} +</style>