mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
ui: update search builder
This commit is contained in:
2 files changed
+65
-70
No files matched your search
@@ -126,6 +126,7 @@
|
|||||||
"markdown-it-table-of-contents": "^0.6.0",
|
"markdown-it-table-of-contents": "^0.6.0",
|
||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"schemastery-jsonschema": "^1.0.3",
|
"schemastery-jsonschema": "^1.0.3",
|
||||||
|
"search-query-parser": "^1.6.0",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"xss": "^1.0.14"
|
"xss": "^1.0.14"
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-70
@@ -1,5 +1,6 @@
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import parser from 'search-query-parser';
|
||||||
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete';
|
||||||
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
|
import { ActionDialog, ConfirmDialog } from 'vj/components/dialog';
|
||||||
import Dropdown from 'vj/components/dropdown/Dropdown';
|
import Dropdown from 'vj/components/dropdown/Dropdown';
|
||||||
@@ -12,8 +13,7 @@ import {
|
|||||||
} from 'vj/utils';
|
} from 'vj/utils';
|
||||||
|
|
||||||
const categories = {};
|
const categories = {};
|
||||||
const dirtyCategories = [];
|
let selections: string[] = [];
|
||||||
const selections = [];
|
|
||||||
const list = [];
|
const list = [];
|
||||||
|
|
||||||
function setDomSelected($dom, selected) {
|
function setDomSelected($dom, selected) {
|
||||||
@@ -21,44 +21,48 @@ function setDomSelected($dom, selected) {
|
|||||||
else $dom.removeClass('selected');
|
else $dom.removeClass('selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSelection(sendRequest = true) {
|
const parserOptions = {
|
||||||
dirtyCategories.forEach(({ type, category, subcategory }) => {
|
keywords: ['category', 'difficulty'],
|
||||||
let item = categories[category];
|
offsets: true,
|
||||||
const isSelected = item.select || _.some(item.children, (c) => c.select);
|
alwaysArray: true,
|
||||||
setDomSelected(item.$tag, isSelected);
|
tokenize: true,
|
||||||
if (isSelected) {
|
};
|
||||||
selections.push(category);
|
|
||||||
} else {
|
function writeSelectionToInput() {
|
||||||
_.pull(selections, category);
|
const currentValue = $('[name="q"]').val() as string;
|
||||||
|
const parsedCurrentValue = parser.parse(currentValue, parserOptions) as parser.SearchParserResult;
|
||||||
|
const q = parser.stringify({
|
||||||
|
...parsedCurrentValue,
|
||||||
|
category: selections,
|
||||||
|
text: parsedCurrentValue.text,
|
||||||
|
}, parserOptions);
|
||||||
|
$('[name="q"]').val(q);
|
||||||
}
|
}
|
||||||
if (type === 'subcategory') {
|
|
||||||
item = categories[category].children[subcategory];
|
function updateSelection() {
|
||||||
setDomSelected(item.$tag, item.select);
|
selections = _.uniq(selections);
|
||||||
const selectionName = subcategory;
|
for (const category in categories) {
|
||||||
if (item.select) {
|
const item = categories[category];
|
||||||
selections.push(selectionName);
|
let childSelected = false;
|
||||||
} else {
|
for (const subcategory in item.children) {
|
||||||
_.pull(selections, selectionName);
|
const shouldSelect = selections.includes(subcategory);
|
||||||
|
const isSelected = item.children[subcategory].$tag.hasClass('selected');
|
||||||
|
childSelected ||= shouldSelect;
|
||||||
|
if (isSelected !== shouldSelect) setDomSelected(item.children[subcategory].$tag, shouldSelect);
|
||||||
|
}
|
||||||
|
const shouldSelect = selections.includes(category) || childSelected;
|
||||||
|
const isSelected = item.$tag.hasClass('selected');
|
||||||
|
if (isSelected !== shouldSelect) setDomSelected(item.$tag, shouldSelect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
dirtyCategories.length = 0;
|
function loadQuery() {
|
||||||
if (sendRequest) {
|
const q = $('[name="q"]').val().toString();
|
||||||
// a list of categories which subcategory is selected
|
|
||||||
const requestCategoryTags = _.uniq(selections
|
|
||||||
.filter((s) => s.indexOf(',') !== -1)
|
|
||||||
.map((s) => s.split(',')[0]));
|
|
||||||
// drop the category if its subcategory is selected
|
|
||||||
const requestTags = _.uniq(_.pullAll(selections, requestCategoryTags));
|
|
||||||
let q = $('[name="q"]').val().split(' ').filter((i) => !i.startsWith('category:')).join(' ');
|
|
||||||
if (requestTags.length) q = requestTags.map((i) => `category:${i}`).join(' ') + (q ? ` ${q}` : '');
|
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
if (!q) url.searchParams.delete('q');
|
if (!q) url.searchParams.delete('q');
|
||||||
else url.searchParams.set('q', q);
|
else url.searchParams.set('q', q);
|
||||||
url.searchParams.delete('page');
|
url.searchParams.delete('page');
|
||||||
pjax.request({ url: url.toString() });
|
pjax.request({ url: url.toString() });
|
||||||
$('[name="q"]').val(q);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCategoryFilter() {
|
function buildCategoryFilter() {
|
||||||
@@ -78,7 +82,6 @@ function buildCategoryFilter() {
|
|||||||
.remove()
|
.remove()
|
||||||
.attr('class', 'widget--category-filter__drop');
|
.attr('class', 'widget--category-filter__drop');
|
||||||
const treeItem = {
|
const treeItem = {
|
||||||
select: false,
|
|
||||||
$tag: $categoryTag,
|
$tag: $categoryTag,
|
||||||
children: {},
|
children: {},
|
||||||
};
|
};
|
||||||
@@ -95,7 +98,6 @@ function buildCategoryFilter() {
|
|||||||
$subCategoryTags.get().forEach((subCategoryTag) => {
|
$subCategoryTags.get().forEach((subCategoryTag) => {
|
||||||
const $tag = $(subCategoryTag);
|
const $tag = $(subCategoryTag);
|
||||||
treeItem.children[$tag.text()] = {
|
treeItem.children[$tag.text()] = {
|
||||||
select: false,
|
|
||||||
$tag,
|
$tag,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -106,25 +108,17 @@ function buildCategoryFilter() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
list.push(...Object.keys(categories));
|
list.push(...Object.keys(categories));
|
||||||
list.push(..._.flatMap(Object.values(categories), (c) => Object.keys(c.children)));
|
list.push(..._.flatMap(Object.values(categories), (c: any) => Object.keys(c.children)));
|
||||||
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
|
$(document).on('click', '.widget--category-filter__category-tag', (ev) => {
|
||||||
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
|
if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return;
|
||||||
const category = $(ev.currentTarget).text();
|
const category = $(ev.currentTarget).text();
|
||||||
const treeItem = categories[category];
|
const treeItem = categories[category];
|
||||||
// the effect should be cancelSelect if it is shown as selected when clicking
|
const shouldSelect = !treeItem.$tag.hasClass('selected');
|
||||||
const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select;
|
if (shouldSelect) selections.push(category);
|
||||||
treeItem.select = shouldSelect;
|
else selections = _.without(selections, category, ...Object.keys(treeItem.children));
|
||||||
dirtyCategories.push({ type: 'category', category });
|
|
||||||
if (!shouldSelect) {
|
|
||||||
// de-select children
|
|
||||||
_.forEach(treeItem.children, (treeSubItem, subcategory) => {
|
|
||||||
if (treeSubItem.select) {
|
|
||||||
treeSubItem.select = false;
|
|
||||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateSelection();
|
updateSelection();
|
||||||
|
writeSelectionToInput();
|
||||||
|
loadQuery();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
});
|
});
|
||||||
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
|
$(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => {
|
||||||
@@ -132,27 +126,21 @@ function buildCategoryFilter() {
|
|||||||
const subcategory = $(ev.currentTarget).text();
|
const subcategory = $(ev.currentTarget).text();
|
||||||
const category = $(ev.currentTarget).attr('data-category');
|
const category = $(ev.currentTarget).attr('data-category');
|
||||||
const treeItem = categories[category].children[subcategory];
|
const treeItem = categories[category].children[subcategory];
|
||||||
treeItem.select = !treeItem.select;
|
const shouldSelect = !treeItem.$tag.hasClass('selected');
|
||||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
if (shouldSelect) selections.push(subcategory);
|
||||||
|
else selections = _.without(selections, subcategory);
|
||||||
|
// TODO auto de-select parent
|
||||||
updateSelection();
|
updateSelection();
|
||||||
|
writeSelectionToInput();
|
||||||
|
loadQuery();
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategorySelection() {
|
function parseCategorySelection() {
|
||||||
UiContext.currentCategory.split(' ').forEach((cline) => {
|
const parsed = parser.parse($('[name="q"]').val() as string || '', parserOptions) as parser.SearchParserResult;
|
||||||
const [category, subcategory] = cline.split(',');
|
selections = _.uniq(parsed.category || []);
|
||||||
if (!categories[category]) return;
|
updateSelection();
|
||||||
if (subcategory && !categories[category].children[subcategory]) return;
|
|
||||||
if (!subcategory) {
|
|
||||||
categories[category].select = true;
|
|
||||||
dirtyCategories.push({ type: 'category', category });
|
|
||||||
} else {
|
|
||||||
categories[category].children[subcategory].select = true;
|
|
||||||
dirtyCategories.push({ type: 'subcategory', subcategory, category });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
updateSelection(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureAndGetSelectedPids() {
|
function ensureAndGetSelectedPids() {
|
||||||
@@ -170,7 +158,7 @@ function ensureAndGetSelectedPids() {
|
|||||||
async function handleOperation(operation) {
|
async function handleOperation(operation) {
|
||||||
const pids = ensureAndGetSelectedPids();
|
const pids = ensureAndGetSelectedPids();
|
||||||
if (pids === null) return;
|
if (pids === null) return;
|
||||||
const payload = {};
|
const payload: any = {};
|
||||||
if (operation === 'delete') {
|
if (operation === 'delete') {
|
||||||
const action = await new ConfirmDialog({
|
const action = await new ConfirmDialog({
|
||||||
$body: tpl.typoMsg(i18n('Confirm to delete the selected problems?')),
|
$body: tpl.typoMsg(i18n('Confirm to delete the selected problems?')),
|
||||||
@@ -194,7 +182,7 @@ async function handleOperation(operation) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`).appendTo(document.body);
|
`).appendTo(document.body);
|
||||||
const domainSelector = DomainSelectAutoComplete.getOrConstruct($('.dialog__body--problem-copy [name="target"]'));
|
const domainSelector: any = DomainSelectAutoComplete.getOrConstruct($('.dialog__body--problem-copy [name="target"]'));
|
||||||
const copyDialog = await new ActionDialog({
|
const copyDialog = await new ActionDialog({
|
||||||
$body: $('.dialog__body--problem-copy > div'),
|
$body: $('.dialog__body--problem-copy > div'),
|
||||||
onDispatch(action) {
|
onDispatch(action) {
|
||||||
@@ -214,7 +202,7 @@ async function handleOperation(operation) {
|
|||||||
await request.post('', { operation, pids, ...payload });
|
await request.post('', { operation, pids, ...payload });
|
||||||
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
|
Notification.success(i18n(`Selected problems have been ${operation === 'copy' ? 'copie' : operation}d.`));
|
||||||
await delay(2000);
|
await delay(2000);
|
||||||
updateSelection(true);
|
loadQuery();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Notification.error(error.message);
|
Notification.error(error.message);
|
||||||
}
|
}
|
||||||
@@ -245,6 +233,7 @@ const page = new NamedPage(['problem_main'], () => {
|
|||||||
$('.section.display-mode').removeClass('display-mode');
|
$('.section.display-mode').removeClass('display-mode');
|
||||||
buildCategoryFilter();
|
buildCategoryFilter();
|
||||||
parseCategorySelection();
|
parseCategorySelection();
|
||||||
|
updateSelection();
|
||||||
$(document).on('click', '[name="leave-edit-mode"]', () => {
|
$(document).on('click', '[name="leave-edit-mode"]', () => {
|
||||||
$body.removeClass('edit-mode').addClass('display-mode');
|
$body.removeClass('edit-mode').addClass('display-mode');
|
||||||
});
|
});
|
||||||
@@ -257,14 +246,19 @@ const page = new NamedPage(['problem_main'], () => {
|
|||||||
$(document).on('click', '[name="download_selected_problems"]', handleDownload);
|
$(document).on('click', '[name="download_selected_problems"]', handleDownload);
|
||||||
|
|
||||||
$(document).on('click', '.toggle-tag', () => {
|
$(document).on('click', '.toggle-tag', () => {
|
||||||
$('.section__table-container').children(1).toggleClass('hide-problem-tag');
|
$('.section__table-container').toggleClass('hide-problem-tag');
|
||||||
});
|
});
|
||||||
|
function inputChanged() {
|
||||||
|
parseCategorySelection();
|
||||||
|
updateSelection();
|
||||||
|
loadQuery();
|
||||||
|
}
|
||||||
$('#search').on('click', (ev) => {
|
$('#search').on('click', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
updateSelection();
|
inputChanged();
|
||||||
});
|
});
|
||||||
$('#searchForm').on('submit', updateSelection);
|
$('#searchForm').on('submit', inputChanged);
|
||||||
$('#searchForm').find('input').on('input', _.debounce(updateSelection, 500));
|
$('#searchForm').find('input').on('input', _.debounce(inputChanged, 500));
|
||||||
$(document).on('click', 'a.pager__item', (ev) => {
|
$(document).on('click', 'a.pager__item', (ev) => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
pjax.request(ev.currentTarget.getAttribute('href')).then(() => window.scrollTo(0, 0));
|
pjax.request(ev.currentTarget.getAttribute('href')).then(() => window.scrollTo(0, 0));
|
||||||
Reference in new issue
Block a user