From f0c5522a266b11db2f310f6f28452d81c5c23d6d Mon Sep 17 00:00:00 2001 From: Joona Yoon Date: Fri, 7 Jan 2022 19:04:47 +0900 Subject: [PATCH] feat: add tabs on quick serach (#55) --- css/common.css | 49 ++++++++++- css/theme-dark.css | 17 ++++ js/search.js | 200 ++++++++++++++++++++++++++++++++++++++------- 3 files changed, 235 insertions(+), 31 deletions(-) diff --git a/css/common.css b/css/common.css index 2f3d50a7..23c1ca0d 100644 --- a/css/common.css +++ b/css/common.css @@ -1,3 +1,4 @@ +/* Problem Timer */ #problem-timer .dropdown-menu { width: 200px; padding: 5px; @@ -13,6 +14,7 @@ margin: 3px; } +/* Quick Search */ #quick-search { display: none; position: fixed; @@ -23,7 +25,7 @@ height: 100vh; justify-content: center; align-items: center; - background: rgba(0, 0, 0, 0.15); + background: rgba(0, 0, 0, 0.075); backdrop-filter: blur(3px); } @@ -69,10 +71,22 @@ } #quick-search .results .quick-search-item b, +#quick-search .results .quick-search-item em, #quick-search .results .quick-search-item strong { background-color: rgba(255, 255, 64, 0.25); } +#quick-search .results .quick-search-item > .search-breadcrumb { + margin-left: 0; + margin-bottom: 3px; +} +#quick-search .results .quick-search-item > .search-breadcrumb > li { + padding: 0; +} +#quick-search .results .quick-search-item > .search-breadcrumb > li + li:before { + content: "/\00a0"; +} + #quick-search .results .quick-search-item > .title { font-size: 1.25em; margin-bottom: 5px; @@ -82,6 +96,11 @@ color: #999; margin-bottom: 5px; } +#quick-search .results .quick-search-item > .meta .date, +#quick-search .results .quick-search-item > .meta .comments, +#quick-search .results .quick-search-item > .meta .author { + margin-right: 0.8em; +} #quick-search .results .quick-search-item > .desc { margin-bottom: 5px; @@ -90,6 +109,10 @@ #quick-search .results .quick-search-item > .links a { margin-right: 1em; } +#quick-search .results .quick-search-item > .links span.tag { + color: #6a9299; + margin-right: 0.5em; +} #quick-search .results-footer { color: #999; @@ -105,3 +128,27 @@ padding-left: 2em; padding-right: 2em; } +#quick-search .tabs .tab { + display: inline-block; + padding: 0.5em 2em; + border: 1px solid transparent; + border-bottom: none; + border-top-left-radius: 5px !important; + border-top-right-radius: 5px !important; + color: #565656; + cursor: pointer; + transition: all 200ms ease-in-out; +} +#quick-search .tabs .tab:hover, +#quick-search .tabs .tab:active { + background-color: rgb(163, 163, 163, 0.5); + border-color: rgb(147, 147, 147, 0.5); + color: #141414; +} +#quick-search .tabs .tab.active { + border-color: #bfbfbf; + background-color: #ffffff; + font-weight: bold; + color: #000000; + cursor: default; +} diff --git a/css/theme-dark.css b/css/theme-dark.css index 620b4b4b..2409a333 100644 --- a/css/theme-dark.css +++ b/css/theme-dark.css @@ -1039,6 +1039,9 @@ html[theme='dark'] .cm-s-paper .CodeMirror-matchingbracket { color: white !important; } /* Quick Search */ +html[theme='dark'] #quick-search { + background: rgba(0, 0, 0, 0.15); +} html[theme='dark'] #quick-search .results { background-color: #262626; border: 1px solid rgba(128, 128, 128, 0.1); @@ -1058,3 +1061,17 @@ html[theme='dark'] #quick-search .results .quick-search-item { html[theme='dark'] #quick-search .results .quick-search-item > .meta { color: #999; } +html[theme='dark'] #quick-search .tabs .tab { + color: #8f8f8f; +} +html[theme='dark'] #quick-search .tabs .tab:hover, +html[theme='dark'] #quick-search .tabs .tab:active { + background-color: rgb(32, 32, 32, 0.7); + border-color: rgb(38, 38, 38, 0.7); + color: #efefef; +} +html[theme='dark'] #quick-search .tabs .tab.active { + border-color: #262626; + background-color: #202020; + color: #ffffff; +} diff --git a/js/search.js b/js/search.js index 70ce5024..51c0084d 100644 --- a/js/search.js +++ b/js/search.js @@ -1,6 +1,5 @@ function extendQuickSearch() { - let isActive = false; - + // UI: overlay const bg = Utils.createElement('div', { id: 'quick-search', class: 'overlay', @@ -30,14 +29,41 @@ function extendQuickSearch() { moreButton.innerText = '더 많은 검색 결과 보기'; form.appendChild(input); form.appendChild(resultBox); + // UI: tabs + const tabsContainer = Utils.createElement('div', { + class: 'tabs', + }); + const tabs = [ + { title: '문제', c: 'Problems', active: true, el: null }, + { title: '문제집', c: 'Workbooks', el: null }, + { title: '출처', c: 'Categories', el: null }, + { title: '블로그', c: 'Blogs', el: null }, + { title: '게시판', c: 'Articles', el: null }, + ]; + for (let i = 0; i < tabs.length; ++i) { + const tab = tabs[i]; + const tabEl = Utils.createElement('div', { class: 'tab', tabIndex: i }); + tabEl.innerText = tab.title; + if (tab.active) tabEl.classList.add('active'); + tabEl.addEventListener('click', (evt) => { + evt.preventDefault(); + activateTab(tabEl.getAttribute('tabIndex')); + }); + tabs[i].el = tabEl; // refer + tabsContainer.appendChild(tabEl); + } + container.appendChild(tabsContainer); container.appendChild(form); container.appendChild(resultFooter); container.appendChild(moreButton); bg.appendChild(container); document.body.appendChild(bg); + // variables let searchHandle = null; let lastSearchText = ''; + let problemInfo = {}; + let currentTabIndex = 0; // add event listener to input input.addEventListener('keyup', async (evt) => { @@ -53,6 +79,7 @@ function extendQuickSearch() { } }); + // handle key event const keyPressed = new Set(); document.addEventListener('keydown', (evt) => { keyPressed.add(evt.key); @@ -60,34 +87,58 @@ function extendQuickSearch() { document.addEventListener('keyup', (evt) => { console.log(evt); if (keyPressed.has('Escape')) { - isActive = false; - bg.style.display = 'none'; + activate(false); } else if ( keyPressed.has('/') && (keyPressed.has('Control') || keyPressed.has('Alt')) ) { - isActive = true; - bg.style.display = 'flex'; - setTimeout(() => { - input.focus(); - }, 10); + activate(true); } keyPressed.delete(evt.key); }); // dismiss search overlay document.addEventListener('click', (evt) => { - if (evt.target == bg) { - isActive = false; + if (evt.target == bg) activate(false); + }); + + async function activate(on) { + if (on === true) { + // fetch problem status by current user + problemInfo = await fetchProblemsByUser(getMyUsername()); + // set variables + bg.style.display = 'flex'; + setTimeout(() => { + input.focus(); + }, 10); + } else { + // deactivate bg.style.display = 'none'; } - }); + } + + function activateTab(tabIndex) { + for (const tab of tabs) { + const isActive = tab.el.getAttribute('tabIndex') == tabIndex; + if (isActive) { + tab.el.classList.add('active'); + } else { + tab.el.classList.remove('active'); + } + } + currentTabIndex = Number(tabIndex); + search(input.value); + } async function search(searchText) { + // scroll to top + resultBox.scroll(0, 0); + // hijack + const currentIndexName = tabs[currentTabIndex].c || 'Problems'; const dataForm = { requests: [ { - indexName: 'Problems', + indexName: currentIndexName, params: encodeURI( 'query=' + searchText + '&page=0&facets=[]&tagFilters=' ), @@ -103,30 +154,119 @@ function extendQuickSearch() { } ).then((res) => res.json()); - resultBox.innerHTML = ''; const { hits, processingTimeMS, nbHits } = results[0]; - console.log(results[0]); + resultBox.innerHTML = ''; + console.groupCollapsed(`${currentIndexName}: "${searchText}"`); + console.log('results', results[0]); for (const res of hits) { - const { id, time, memory } = res; - const { title, description } = res._highlightResult; - const item = Utils.createElement('div', { class: 'quick-search-item' }); - item.innerHTML = `\ -
${id}번 - ${title.value}
\ -
시간 제한: ${time}초   메모리 제한: ${memory}MB
\ -
${description.value}
\ - \ - `; - resultBox.appendChild(item); + const item = createResultItem(res, currentIndexName); + if (item) resultBox.appendChild(item); + console.log(res); } - moreButton.href = encodeURI('/search#q=' + searchText + '&c=Problems'); + console.groupEnd(); + moreButton.href = encodeURI('/search#q=' + searchText + '&c=' + currentIndexName); resultFooter.innerHTML = `${nbHits}개의 결과 중 ${hits.length}개 표시 (${ processingTimeMS / 1000 }초)`; } + + function createResultItem(result, indexName) { + if (result == null) return null; + switch (indexName) { + case 'Problems': + return createItemFromHTML(htmlProblems(result)); + case 'Workbooks': + return createItemFromHTML(htmlWorkbooks(result)); + case 'Categories': + return createItemFromHTML(htmlCategories(result)); + case 'Blogs': + return createItemFromHTML(htmlBlogs(result)); + case 'Articles': + return createItemFromHTML(htmlArticles(result)); + default: + return null; + } + } + + function createItemFromHTML(html) { + const item = Utils.createElement('div', { class: 'quick-search-item' }); + item.innerHTML = html; + return item; + } + + function htmlProblems(result) { + const { id, time, memory, _highlightResult } = result; + const { title, description } = _highlightResult; + return `\ +
${id}번 - ${title.value}
\ +
시간 제한: ${time}초   메모리 제한: ${memory}MB
\ +
${description.value}
\ + \ + `; + } + function htmlWorkbooks(result) { + const { id, problems, _highlightResult } = result; + const { name, comment, creator, problem } = _highlightResult; + return `\ +
${name.value}
\ +
만든 사람: ${creator.value}   문제: ${problems}
\ +
${comment.value}
\ + `; + } + function htmlCategories(result) { + const { avail, id, parents, total, _highlightResult } = result; + const { name } = _highlightResult; + const breadcrumb = (parents || []).map(child => `
  • ${child.name}
  • `).join('\n'); + return `\ + +
    ${name.value}
    \ +
    전체 문제: ${total}   풀 수 있는 문제: ${avail}
    \ + `; + } + function htmlBlogs(result) { + const { id, user, comments, date, tags } = result; + const { title } = result._highlightResult; + const { content } = result._snippetResult; + const tagList = tags.filter(tag => tag.length > 0).map(tag => `#${tag}`).join('\n'); + return `\ +
    ${title.value}
    \ +
    \ + ${date}\ + ${user}\ + ${comments}\ +
    \ +
    ${content.value}
    \ + \ + `; + } + function htmlArticles(result) { + const { id, problem, category, created, user, comments, like } = result; + const { subject } = result._highlightResult; + const { content } = result._snippetResult; + let problemTag = ''; + let problemColor = ''; + if (problem != null) { + problemColor = problemInfo[problem] || ''; + problemTag = `${problem}번 `; + } + return `\ +
    \ + ${problemTag}${category}\ +
    +
    ${subject.value}
    \ +
    \ + ${created}\ + ${user}\ + ${comments}\ + ${like}\ +
    \ +
    ${content.value}
    \ + `; + } } async function extendSearchPage() {