diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index 3857b9036..c80f00cce 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -518,8 +518,8 @@ Numeric PID: 数字题号 Oh, the user doesn't have any contributions!: 啊哦,这个用户还没贡献过题目~ Oh, the user hasn't created any discussions yet!: 这个用户还没有发布过讨论 Oh, the user hasn't submitted yet!: 这个用户还没有交过题 _(:зゝ∠)_ -Oh, there is no task in the queue!: 喔,队列中目前没有任务。 Oh, there are no tasks that match the filter!: 喔,目前没有符合过滤条件的任务。 +Oh, there is no task in the queue!: 喔,队列中目前没有任务。 Ok: 确定 Only A-Z, a-z, 0-9 and _ are accepted: 只接受 A-Z, a-z, 0-9 和 _ Oops, there are no results.: 呀,没有结果。 @@ -582,6 +582,7 @@ Preview: 预览 Privacy: 隐私 Problem {1} not found.: 题目 {1} 不存在。 Problem Categories: 题目标签 +Problem Category: 标签 Problem Data: 题目数据 Problem Description: 题目描述 Problem ID List: 题目 ID 列表 @@ -684,7 +685,9 @@ Section: 章节 Security: 安全 Select a node to create discussion.: 选择一个节点来发表讨论。 Select a role: 选择一个角色 +Select Category: 选择标签 Select User: 选择用户 +Selected categories: 已选标签 Selected roles have been deleted.: 所选角色已删除。 Selected users have been removed from the domain.: 所选用户已从此域中移除。 Send Code after acceptance: 通过题目后发送源代码 diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index f2ab458a8..85976ff1b 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -646,6 +646,7 @@ Pretest?: 自测? Privacy: 隐私 Privilege: 权限 Problem {1} not found.: 题目 {1} 不存在。 +Problem Category: 标签 Problem Data: 题目数据 Problem ID cannot be a pure number. Leave blank if you want to use numberic id.: 题目ID不能为纯数字。 若要自动分配数字ID请将此处留空。 Problem ID List: 题目 ID 列表 @@ -767,7 +768,9 @@ Section: 章节 Security: 安全 Select a node to create discussion.: 选择一个节点来发表讨论。 Select a role: 选择一个角色 +Select Category: 选择标签 Select User: 选择用户 +Selected categories: 已选标签 Selected files have been deleted.: 所选文件已被删除。 Selected problems have been deleted.: 所选题目已被删除。 Selected roles have been deleted.: 所选角色已删除。 diff --git a/packages/ui-default/misc/structure.styl b/packages/ui-default/misc/structure.styl index 2476e2151..87a3e23d4 100644 --- a/packages/ui-default/misc/structure.styl +++ b/packages/ui-default/misc/structure.styl @@ -125,6 +125,9 @@ html display: flex flex-direction: column +.flex-wrap + flex-wrap: wrap + .flex-fill flex: 1 min-width: 0 diff --git a/packages/ui-default/pages/problem_main.page.styl b/packages/ui-default/pages/problem_main.page.styl index b0cc944a4..9b4a4c887 100644 --- a/packages/ui-default/pages/problem_main.page.styl +++ b/packages/ui-default/pages/problem_main.page.styl @@ -38,19 +38,6 @@ &__category margin-bottom: rem(6px) - &-tag - display: inline-block - padding: rem(5px 6px) - - &:hover - color: #FFF - background: $primary-color - text-decoration: none - - &.selected - color: #FFF - background: $secondary-color - &__drop position: relative font-size: rem($font-size-small) @@ -80,19 +67,19 @@ &__subcategory margin: rem(2px) display: inline-block + + &__tag + display: inline-block + padding: rem(5px 6px) - &-tag - display: inline-block - padding: rem(5px 6px) + &:hover + color: #FFF + background: $primary-color + text-decoration: none - &:hover - color: #FFF - background: $primary-color - text-decoration: none - - &.selected - color: #FFF - background: $secondary-color + &.selected + color: #FFF + background: $secondary-color .drop-target-attached-right .widget--category-filter__drop margin-right: 0 @@ -126,6 +113,82 @@ .data-table > thead > tr height: 54.9px +.search-container + padding-bottom: 0 + + .title + margin-bottom: rem(5px) + + .dialog-button + font-size: rem($font-size-secondary) + display: inline-block + padding: rem(5px 6px) + margin-bottom: rem(5px) + +.search-tag__item + display: inline-block + padding: rem(5px 6px) + font-size: rem($font-size-secondary) + margin-bottom: rem(5px) + + &:hover + color: #555; + background: #f4f4f4; + text-decoration: none + + &.selected + color: #FFF + background: $primary-color !important + + &.hide + display: none + opacity: 0 + +.category-filter__header + [data-category-container].list + overflow-x: auto + + .list + .list__item + padding: 12px 12px + cursor: pointer + margin: 0px 10px + background: #eeeff3 + min-width: 90px + text-align: center + position: relative + + &.selected + background: $primary-color + color: #FFF + font-weight: bold + + .list__item::first + margin-left: 0px + +.category-filter__body + margin: 12px 10px 0 10px + background: #f7f7f7 + max-height: rem(400px) + overflow-y: auto + + .subcategory-container + &.hide + display: none + opacity: 0 + + & h2 + font-size: rem($font-size) + font-weight: bold + margin-bottom: 16px + + .divider + border-bottom: 1px solid #e5e5e5 + margin: rem(16px) 0 + + .search-tag__item + background: #eaeaea; + .search width: 100% position: relative diff --git a/packages/ui-default/pages/problem_main.page.ts b/packages/ui-default/pages/problem_main.page.ts index 2c5af165c..b97a56f72 100644 --- a/packages/ui-default/pages/problem_main.page.ts +++ b/packages/ui-default/pages/problem_main.page.ts @@ -2,7 +2,7 @@ import parser, { SearchParserResult } from '@hydrooj/utils/lib/search'; import $ from 'jquery'; import _ from 'lodash'; import DomainSelectAutoComplete from 'vj/components/autocomplete/DomainSelectAutoComplete'; -import { ActionDialog, ConfirmDialog } from 'vj/components/dialog'; +import { ActionDialog, ConfirmDialog, Dialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; import createHint from 'vj/components/hint'; import Notification from 'vj/components/notification'; @@ -12,13 +12,19 @@ import { delay, i18n, pjax, request, tpl, } from 'vj/utils'; -const categories = {}; -let selections: string[] = []; const list = []; +const pinned: Record = { category: [], difficulty: [] }; +const selections = { category: {}, difficulty: {} }; +const selectedTags: Record = { category: [], difficulty: [] }; -function setDomSelected($dom, selected) { - if (selected) $dom.addClass('selected'); - else $dom.removeClass('selected'); +function setDomSelected($dom, selected, icon?) { + if (selected) { + $dom.addClass('selected'); + if (icon) $dom.append(icon); + } else { + $dom.removeClass('selected'); + if (icon) $dom.find('span').remove(); + } } const parserOptions = { @@ -33,26 +39,40 @@ function writeSelectionToInput() { const parsedCurrentValue = parser.parse(currentValue, parserOptions) as SearchParserResult; const q = parser.stringify({ ...parsedCurrentValue, - category: selections, + category: selectedTags.category, + difficulty: selectedTags.difficulty, text: parsedCurrentValue.text, }, parserOptions); $('[name="q"]').val(q); } function updateSelection() { - selections = _.uniq(selections); - for (const category in categories) { - const item = categories[category]; - let childSelected = false; - for (const subcategory in item.children) { - const shouldSelect = selections.includes(subcategory); - const isSelected = item.children[subcategory].$tag.hasClass('selected'); - childSelected ||= shouldSelect; - if (isSelected !== shouldSelect) setDomSelected(item.children[subcategory].$tag, shouldSelect); + selectedTags.category = _.uniq(selectedTags.category); + for (const type in selections) { + for (const selection in selections[type]) { + const item = selections[type][selection]; + const shouldSelect = selectedTags[type].includes(selection); + const isSelected = (item.$tag || item.$legacy).hasClass('selected'); + let childSelected = false; + for (const subcategory in item.children) { + const childShouldSelect = selectedTags[type].includes(subcategory); + const childIsSelected = item.children[subcategory].$tag.hasClass('selected'); + childSelected ||= childShouldSelect; + if (childIsSelected !== childShouldSelect) setDomSelected(item.children[subcategory].$tag, childShouldSelect); + } + if (item.$legacy) setDomSelected(item.$legacy, (shouldSelect || childSelected)); + if (isSelected !== shouldSelect) { + if (pinned[type].includes(selection)) { + setDomSelected(item.$tag, shouldSelect, ''); + } else { + if (item.$tag) setDomSelected(item.$tag, shouldSelect, ''); + for (const $element of item.$phantom) { + if (shouldSelect) $($element).removeClass('hide'); + else $($element).addClass('hide'); + } + } + } } - const shouldSelect = selections.includes(category) || childSelected; - const isSelected = item.$tag.hasClass('selected'); - if (isSelected !== shouldSelect) setDomSelected(item.$tag, shouldSelect); } } @@ -65,7 +85,22 @@ function loadQuery() { pjax.request({ url: url.toString() }); } -function buildCategoryFilter() { +function handleTagSelected(ev) { + if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return; + let [type, selection] = ['category', $(ev.currentTarget).text()]; + if ($(ev.currentTarget).attr('data-selection')) [type, selection] = $(ev.currentTarget).attr('data-selection').split(':'); + const category = $(ev.currentTarget).attr('data-category'); + const treeItem = category ? selections[type][category].children[selection] : selections[type][selection]; + const shouldSelect = !(treeItem.$tag || treeItem.$legacy).hasClass('selected'); + if (shouldSelect) selectedTags[type].push(selection); + else selectedTags[type] = _.without(selectedTags[type], selection, ...(category ? [] : Object.keys(treeItem.children))); + updateSelection(); + writeSelectionToInput(); + loadQuery(); + ev.preventDefault(); +} + +function buildLegacyCategoryFilter() { const $container = $('[data-widget-cf-container]'); if (!$container) return; $container.attr('class', 'widget--category-filter row small-up-3 medium-up-2'); @@ -75,17 +110,22 @@ function buildCategoryFilter() { const $categoryTag = $category .find('.section__title a') .remove() - .attr('class', 'widget--category-filter__category-tag'); + .attr('class', 'widget--category-filter__tag'); const categoryText = $categoryTag.text(); const $drop = $category .children('.chip-list') .remove() .attr('class', 'widget--category-filter__drop'); - const treeItem = { - $tag: $categoryTag, - children: {}, - }; - categories[categoryText] = treeItem; + if (selections.category[categoryText]) { + selections.category[categoryText].$legacy = $categoryTag; + } else { + selections.category[categoryText] = { + $legacy: $categoryTag, + $tag: null, + children: {}, + $phantom: [], + }; + } $category.empty().append($categoryTag); if ($drop.length > 0) { $categoryTag.text(`${$categoryTag.text()}`); @@ -93,11 +133,11 @@ function buildCategoryFilter() { .children('li') .attr('class', 'widget--category-filter__subcategory') .find('a') - .attr('class', 'widget--category-filter__subcategory-tag') + .attr('class', 'widget--category-filter__tag') .attr('data-category', categoryText); $subCategoryTags.get().forEach((subCategoryTag) => { const $tag = $(subCategoryTag); - treeItem.children[$tag.text()] = { + selections.category[categoryText].children[$tag.text()] = { $tag, }; }); @@ -107,40 +147,15 @@ function buildCategoryFilter() { }); } }); - list.push(...Object.keys(categories)); - list.push(..._.flatMap(Object.values(categories), (c: any) => Object.keys(c.children))); - $(document).on('click', '.widget--category-filter__category-tag', (ev) => { - if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return; - const category = $(ev.currentTarget).text(); - const treeItem = categories[category]; - const shouldSelect = !treeItem.$tag.hasClass('selected'); - if (shouldSelect) selections.push(category); - else selections = _.without(selections, category, ...Object.keys(treeItem.children)); - updateSelection(); - writeSelectionToInput(); - loadQuery(); - ev.preventDefault(); - }); - $(document).on('click', '.widget--category-filter__subcategory-tag', (ev) => { - if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return; - const subcategory = $(ev.currentTarget).text(); - const category = $(ev.currentTarget).attr('data-category'); - const treeItem = categories[category].children[subcategory]; - const shouldSelect = !treeItem.$tag.hasClass('selected'); - if (shouldSelect) selections.push(subcategory); - else selections = _.without(selections, subcategory); - // TODO auto de-select parent - updateSelection(); - writeSelectionToInput(); - loadQuery(); - ev.preventDefault(); - }); + list.push(...Object.keys(selections.category)); + list.push(..._.flatMap(Object.values(selections.category), (c: any) => Object.keys(c.children))); + $(document).on('click', '.widget--category-filter__tag', (ev) => handleTagSelected(ev)); } function parseCategorySelection() { const parsed = parser.parse($('[name="q"]').val() as string || '', parserOptions) as SearchParserResult; - selections = _.uniq(parsed.category || []); - updateSelection(); + selectedTags.category = _.uniq(parsed.category || []); + selectedTags.difficulty = _.uniq(parsed.difficulty || []); } function ensureAndGetSelectedPids() { @@ -214,6 +229,54 @@ function hideTags(target) { .forEach((i) => $(i).addClass('notag--hide')); } +const categoryDialog: any = new Dialog({ + $body: $('.dialog--category-filter'), + cancelByClickingBack: true, + cancelByEsc: true, +}); + +categoryDialog.clear = function () { + const $dataCategoryContainer = this.$dom.find('[data-category-container]'); + $dataCategoryContainer.children().addClass('hide'); + const first = $dataCategoryContainer.children().first(); + setDomSelected(first, true); + this.$dom.find('[data-subcategory-container]').addClass('hide'); + this.$dom.find(`[data-subcategory-container="${first.attr('data-category')}"]`).removeClass('hide'); + return this; +}; + +function buildSearchContainer() { + $('[data-pinned-container] [data-selection]').each((_index, _element) => { + const [type, selection] = $(_element).attr('data-selection').split(':'); + pinned[type].push(selection); + selections[type][selection] = { + $tag: $(_element), + children: {}, + }; + }); + + categoryDialog.$dom.find('.subcategory__all .search-tag__item').each((_index, _element) => { + const [,subcategory] = $(_element).attr('data-selection').split(':'); + selections.category[subcategory] = { + $tag: $(_element), + children: {}, + $phantom: [ + ...categoryDialog.$dom.find(`.subcategory__selected .search-tag__item[data-selection="category:${subcategory}"]`).get(), + ...$(`.subcategory-container__selected .search-tag__item[data-selection="category:${subcategory}"]`).get(), + ], + }; + }); + + $(document).on('click', '[data-category-container] [data-category]', (ev) => { + $('[data-category-container] [data-category]').removeClass('selected'); + $(ev.currentTarget).addClass('selected'); + $('[data-subcategory-container]').addClass('hide'); + $(`[data-subcategory-container="${$(ev.currentTarget).attr('data-category')}"]`).removeClass('hide'); + }); + + $(document).on('click', '.search-tag__item', (ev) => handleTagSelected(ev)); +} + async function handleDownload(ev) { let name = 'Export'; // eslint-disable-next-line no-alert @@ -231,7 +294,9 @@ const page = new NamedPage(['problem_main'], () => { const $body = $('body'); $body.addClass('display-mode'); $('.section.display-mode').removeClass('display-mode'); - buildCategoryFilter(); + + buildSearchContainer(); + buildLegacyCategoryFilter(); parseCategorySelection(); updateSelection(); $(document).on('click', '[name="leave-edit-mode"]', () => { @@ -246,7 +311,7 @@ const page = new NamedPage(['problem_main'], () => { $(document).on('click', '[name="download_selected_problems"]', handleDownload); $(document).on('click', '.toggle-tag', () => { - $('.section__table-container').toggleClass('hide-problem-tag'); + $('.section__table-container').children().toggleClass('hide-problem-tag'); }); function inputChanged() { parseCategorySelection(); @@ -259,6 +324,10 @@ const page = new NamedPage(['problem_main'], () => { }); $('#searchForm').on('submit', inputChanged); $('#searchForm').find('input').on('input', _.debounce(inputChanged, 500)); + $('.dialog-button').on('click', (ev) => { + categoryDialog.clear().open(); + ev.preventDefault(); + }); $(document).on('click', 'a.pager__item', (ev) => { ev.preventDefault(); pjax.request(ev.currentTarget.getAttribute('href')).then(() => window.scrollTo(0, 0)); diff --git a/packages/ui-default/templates/partials/category.html b/packages/ui-default/templates/partials/category.html index fd507f421..27196b4cf 100644 --- a/packages/ui-default/templates/partials/category.html +++ b/packages/ui-default/templates/partials/category.html @@ -1,8 +1,9 @@