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., '&times; 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, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
-            },
-            // 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>