import $ from 'jquery'; import _ from 'lodash'; import { ConfirmDialog } from 'vj/components/dialog'; import Dropdown from 'vj/components/dropdown/Dropdown'; import Editor from 'vj/components/editor/index'; import Notification from 'vj/components/notification'; import uploadFiles from 'vj/components/upload'; import download from 'vj/components/zipDownloader'; import { NamedPage } from 'vj/misc/Page'; import { i18n, pjax, request, slideDown, slideUp, tpl, } from 'vj/utils'; const categories = {}; const dirtyCategories = []; const selections = []; const tags = []; function setDomSelected($dom, selected) { if (selected) $dom.addClass('selected'); else $dom.removeClass('selected'); } async function updateSelection() { dirtyCategories.forEach(({ type, category, subcategory }) => { let item = categories[category]; const isSelected = item.select || _.some(item.children, (c) => c.select); setDomSelected(item.$tag, isSelected); if (isSelected) selections.push(category); else _.pull(selections, category); if (type === 'subcategory') { item = categories[category].children[subcategory]; setDomSelected(item.$tag, item.select); const selectionName = subcategory; if (item.select) selections.push(selectionName); else _.pull(selections, selectionName); } }); 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)); dirtyCategories.length = 0; const $txt = $('[name="tag"]'); $txt.val([...requestTags, ...tags].join(', ')); } function findCategory(name) { const keys = Object.keys(categories); if (keys.includes(name)) return [name, null]; for (const category of keys) { const subkeys = Object.keys(categories[category].children); if (subkeys.includes(name)) return [category, name]; } return [null, null]; } function parseCategorySelection() { const $txt = $('[name="tag"]'); tags.length = 0; $txt.val().split(',').map((name) => name.trim()).forEach((name) => { if (!name) return; const [category, subcategory] = findCategory(name); if (!category) tags.push(name); else 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(); } function buildCategoryFilter() { const $container = $('[data-widget-cf-container]'); if (!$container) return; $container.attr('class', 'widget--category-filter row small-up-3 medium-up-2'); $container.children('li').get().forEach((category) => { const $category = $(category) .attr('class', 'widget--category-filter__category column'); const $categoryTag = $category .find('.section__title a') .remove() .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 = { select: false, $tag: $categoryTag, children: {}, }; categories[categoryText] = treeItem; $category.empty().append($categoryTag); if ($drop.length > 0) { const $subCategoryTags = $drop .children('li') .attr('class', 'widget--category-filter__subcategory') .find('a') .attr('class', 'widget--category-filter__tag') .attr('data-category', categoryText); $subCategoryTags.get().forEach((subCategoryTag) => { const $tag = $(subCategoryTag); treeItem.children[$tag.text()] = { select: false, $tag }; }); Dropdown.getOrConstruct($categoryTag, { target: $drop[0], position: 'left center', }); } }); $(document).on('click', '.widget--category-filter__tag', (ev) => { if (ev.shiftKey || ev.metaKey || ev.ctrlKey) return; const tag = $(ev.currentTarget).text(); const category = $(ev.currentTarget).attr('data-category'); const treeItem = category ? categories[category].children[tag] : categories[tag]; // the effect should be cancelSelect if it is shown as selected when clicking const shouldSelect = treeItem.$tag.hasClass('selected') ? false : !treeItem.select; treeItem.select = shouldSelect; dirtyCategories.push(category ? { type: 'subcategory', subcategory: tag, category } : { type: 'category', category: tag }); if (!category && !shouldSelect) { // de-select children _.forEach(treeItem.children, (treeSubItem, subcategory) => { if (treeSubItem.select) { treeSubItem.select = false; dirtyCategories.push({ type: 'subcategory', subcategory, category: tag }); } }); } updateSelection(); ev.preventDefault(); }); } async function handleSection(ev, sidebar, type) { const $section = $(ev.currentTarget).closest(`.section--problem-sidebar-${sidebar}`); if ($section.is(`.${type}d, .animating`)) return; $section.addClass('animating'); const $detail = $section.find(`.section--problem-sidebar-${sidebar}__detail`); if (type === 'expand') { await slideDown($detail, 300, { opacity: 0 }, { opacity: 1 }); } else { await slideUp($detail, 300, { opacity: 1 }, { opacity: 0 }); } $section.addClass(type === 'expand' ? 'expanded' : 'collapsed'); $section.removeClass(type === 'expand' ? 'collapsed' : 'expanded'); $section.removeClass('animating'); } export default new NamedPage(['problem_create', 'problem_edit'], (pagename) => { let confirmed = false; $(document).on('click', '[name="operation"]', (ev) => { ev.preventDefault(); if (confirmed) { return request.post('.', { operation: 'delete' }).then((res) => { window.location.href = res.url; }).catch((e) => { Notification.error(e.message); }); } const message = 'Confirm deleting this problem? Its files, submissions, discussions and solutions will be deleted as well.'; return new ConfirmDialog({ $body: tpl`

${i18n(message)}

`, }).open().then((action) => { if (action !== 'yes') return; confirmed = true; ev.target.click(); }); }); $(document).on('change', '[name="tag"]', parseCategorySelection); buildCategoryFilter(); parseCategorySelection(); async function handleClickUpload() { const input = document.createElement('input'); input.type = 'file'; input.multiple = true; input.click(); await new Promise((resolve) => { input.onchange = resolve; }); await uploadFiles('./files', input.files, { type: 'additional_file', sidebar: true, pjax: true, }); } async function handleClickRemove(ev) { const file = [$(ev.currentTarget).parent().parent().attr('data-filename')]; const action = await new ConfirmDialog({ $body: tpl.typoMsg(i18n('Confirm to delete the file?')), }).open(); if (action !== 'yes') return; try { await request.post('./files', { operation: 'delete_files', files: file, type: 'additional_file', }); Notification.success(i18n('File have been deleted.')); await pjax.request({ url: './files?d=additional_file&sidebar=true', push: false }); } catch (error) { Notification.error(error.message); } } async function handleClickDownloadAll() { const files = $('.additional_file-table tr').map(function () { return $(this).attr('data-filename'); }).get(); const { links, pdoc } = await request.post('./files', { operation: 'get_links', files, type: 'additional_file' }); const targets = []; for (const filename of Object.keys(links)) targets.push({ filename, url: links[filename] }); await download(`${pdoc.docId} ${pdoc.title}.zip`, targets); } setInterval(() => { $('img').each(function () { if (this.src.startsWith('file://')) { $(this).attr('src', $(this).attr('src').replace('file://', (pagename === 'problem_create' ? `/file/${UserContext._id}/` : './file/'))); } }); }, 500); const $main = $('textarea[data-editor]'); const $field = $('textarea[data-markdown-upload]'); let content = $field.val(); let isObject = false; let activeTab = $('[data-lang]').first().attr('data-lang'); try { content = JSON.parse(content); isObject = !(content instanceof Array); if (!isObject) content = JSON.stringify(content); } catch (e) { } if (!isObject) content = { [activeTab]: content }; function getContent(lang) { let c = ''; if (content[lang]) c = content[lang]; else { const list = Object.keys(content).filter((l) => l.startsWith(lang)); if (list.length) c = content[list[0]]; } if (typeof c !== 'string') c = JSON.stringify(c); return c; } $main.val(getContent(activeTab)); function onChange(val) { try { val = JSON.parse(val); if (!(val instanceof Array)) val = JSON.stringify(val); } catch { } const empty = /^\s*$/g.test(val); if (empty) delete content[activeTab]; else content[activeTab] = val; if (!Object.keys(content).length) $field.text(''); else $field.text(JSON.stringify(content)); } const editor = Editor.getOrConstruct($main, { onChange }); $('[data-lang]').on('click', (ev) => { $('[data-lang]').removeClass('tab--active'); $(ev.currentTarget).addClass('tab--active'); const lang = $(ev.currentTarget).attr('data-lang'); activeTab = lang; const val = getContent(lang); editor.value(val); }); $('[type="submit"]').on('click', (ev) => { if (!$('[name="title"]').val().toString().length) { Notification.error(i18n('Title is required.')); $('body').scrollTop(); $('html, body').animate( { scrollTop: 0 }, 300, () => $('[name="title"]').focus(), ); ev.preventDefault(); } }); $(document).on('click', '[name="additional_file__upload"]', () => handleClickUpload()); $(document).on('click', '[name="additional_file__delete"]', (ev) => handleClickRemove(ev)); $(document).on('click', '[name="additional_file__download"]', () => handleClickDownloadAll()); $(document).on('click', '[name="additional_file__section__expand"]', (ev) => handleSection(ev, 'additional_file', 'expand')); $(document).on('click', '[name="additional_file__section__collapse"]', (ev) => handleSection(ev, 'additional_file', 'collapse')); $(document).on('click', '[name="tags__section__expand"]', (ev) => handleSection(ev, 'tags', 'expand')); $(document).on('click', '[name="tags__section__collapse"]', (ev) => handleSection(ev, 'tags', 'collapse')); });