Skip to content
Snippets Groups Projects
Commit c8553003 authored by Jan-Hendrik Willms's avatar Jan-Hendrik Willms Committed by Jan-Hendrik Willms
Browse files

implement files and folder filter in plain vue and activate on course and...

implement files and folder filter in plain vue and activate on course and institutes flat file view as well

Closes #1007

Merge request studip/studip!581
parent a8d39359
No related branches found
No related tags found
No related merge requests found
......@@ -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);
}
......
......@@ -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);
}
......
......@@ -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();
}
......@@ -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,
......
......@@ -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": {
......@@ -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",
......
......@@ -35,22 +35,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(
......
......@@ -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';
......
/**
* 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);
......@@ -444,10 +444,6 @@ table.documents {
}
}
&.flat td.filter-match {
background-color: @yellow-20;
}
tr:target {
background-color: @activity-color-20;
}
......
<template>
<div>
<table class="default documents"
:data-folder_id="topfolder.folder_id"
data-shiftcheck>
......@@ -10,7 +11,7 @@
role="clickable"
class="text-bottom"
size="30"></studip-icon>
<span v-if="breadcrumbs.length == 1">
<span v-if="breadcrumbs.length === 1">
{{ breadcrumbs[0].name }}
</span>
</a>
......@@ -28,19 +29,19 @@
</caption>
<colgroup>
<col v-if="show_bulk_actions" width="30px" data-filter-ignore>
<col width="60px" data-filter-ignore>
<col v-if="show_bulk_actions" style="width: 30px">
<col style="width: 60px">
<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 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 width="80px" data-filter-ignore>
<col style="width: 80px">
</colgroup>
<thead>
<tr class="sortable">
......@@ -95,12 +96,17 @@
</tr>
</thead>
<tbody class="subfolders">
<tr v-if="files.length + folders.length == 0" class="empty">
<tr v-if="!hasData" class="empty">
<td :colspan="numberOfColumns">
{{ $gettext('Dieser Ordner ist leer') }}
</td>
</tr>
<tr v-for="folder in sortedFolders"
<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">
......@@ -115,18 +121,20 @@
<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 :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 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 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>
......@@ -143,7 +151,7 @@
</tr>
</tbody>
<tbody class="files">
<tr v-for="file in sortedFiles"
<tr v-for="file in displayedFiles"
:class="file.new ? 'new' : ''"
:id="'fileref_' + file.id"
role="row"
......@@ -166,8 +174,10 @@
class="lightbox-image"
data-lightbox="gallery"></a>
</td>
<td>
<a :href="file.details_url" data-dialog>{{file.name}}</a>
<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"
......@@ -183,13 +193,11 @@
class="responsive-hidden">
{{file.downloads}}
</td>
<td v-if="file.author_url" class="responsive-hidden" >
<a :href="file.author_url">
{{file.author_name}}
<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>
</td>
<td v-else class="responsive-hidden">
{{file.author_name}}
<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>
......@@ -220,8 +228,15 @@
</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>
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment