mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-08-07 22:45:23 +02:00
Merge 86d26baa8e
into 51fb6655e2
This commit is contained in:
commit
f963f133f6
14 changed files with 771 additions and 6 deletions
|
@ -8,5 +8,5 @@
|
|||
"watch": [
|
||||
"**/*"
|
||||
],
|
||||
"ext": "js,twig"
|
||||
"ext": "ts,js,twig"
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/preset-env": "^7.16.11",
|
||||
"@codexteam/misprints": "^1.0.0",
|
||||
"@codexteam/shortcuts": "^1.2.0",
|
||||
"@editorjs/checklist": "^1.3.0",
|
||||
"@editorjs/code": "^2.7.0",
|
||||
"@editorjs/delimiter": "^1.2.0",
|
||||
|
@ -83,6 +84,7 @@
|
|||
"@types/sinon": "^10.0.2",
|
||||
"@types/twig": "^1.12.6",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"axios": "^0.27.2",
|
||||
"babel": "^6.23.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-loader": "^8.2.3",
|
||||
|
|
302
src/backend/controllers/search.ts
Normal file
302
src/backend/controllers/search.ts
Normal file
|
@ -0,0 +1,302 @@
|
|||
import PageData from '../models/page.js';
|
||||
import Pages from '../controllers/pages.js';
|
||||
import urlify from '../utils/urlify.js';
|
||||
import Page from '../models/page.js';
|
||||
|
||||
let globalWords: { [key: string]: {[key: string]: number} } = Object.create(null);
|
||||
let globalPages: PageData[] = [];
|
||||
|
||||
class Search {
|
||||
/**
|
||||
* Initialize search
|
||||
*/
|
||||
public async init() {
|
||||
if (globalWords && Object.keys(globalWords).length) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
await this.syncDB();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all pages from DB and update globalWords
|
||||
* Use this method when any page was updated
|
||||
*/
|
||||
public async syncDB() {
|
||||
globalWords = Object.create(null);
|
||||
globalPages = await this.getPages();
|
||||
|
||||
/**
|
||||
* Process all pages
|
||||
*/
|
||||
for await (const page of globalPages) {
|
||||
/**
|
||||
* Read content blocks from page
|
||||
*/
|
||||
for await (const block of page.body.blocks) {
|
||||
const blockRatio = this.getBlockRatio(block);
|
||||
const blockContent = this.getCleanTextFromBlock(block);
|
||||
const blockWords: string[] = this.splitTextToWords(blockContent);
|
||||
|
||||
/**
|
||||
* Process list of words in a block
|
||||
*/
|
||||
for await (const word of blockWords) {
|
||||
if (!globalWords[word]) {
|
||||
globalWords[word] = Object.create(null);
|
||||
}
|
||||
|
||||
if (page._id) {
|
||||
if (!globalWords[word][page._id]) {
|
||||
globalWords[word][page._id] = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add page id to the list of pages with this word
|
||||
*/
|
||||
globalWords[word][page._id] += blockRatio;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Done');
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for pages by given query
|
||||
* @param searchString
|
||||
*/
|
||||
public async query(searchString: string) {
|
||||
await this.init();
|
||||
|
||||
const searchWords = this.splitTextToWords(searchString);
|
||||
|
||||
const goodPages = (await this.getPagesByWords(searchWords))
|
||||
.slice(0, 10);
|
||||
|
||||
const returnPages: {[key: string]: string|number, ratio: number}[] = [];
|
||||
|
||||
goodPages.forEach(({ pageId, ratio }) => {
|
||||
const page = globalPages.filter(page => page._id === pageId).pop();
|
||||
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
|
||||
let section = '';
|
||||
|
||||
page.body.blocks.forEach((block: any) => {
|
||||
let koef = 1;
|
||||
|
||||
let blockContent = this.getCleanTextFromBlock(block);
|
||||
|
||||
let shortBody = blockContent;
|
||||
|
||||
if (block.type === 'header') {
|
||||
section = blockContent;
|
||||
}
|
||||
|
||||
searchWords.forEach(word => {
|
||||
if (blockContent.toLowerCase().indexOf(word) !== -1) {
|
||||
koef *= 10;
|
||||
}
|
||||
})
|
||||
|
||||
shortBody = this.highlightSubstring(shortBody, searchWords);
|
||||
|
||||
if (koef > 0) {
|
||||
returnPages.push({
|
||||
...page,
|
||||
shortBody,
|
||||
anchor: urlify(section),
|
||||
section,
|
||||
ratio: ratio * koef,
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
suggestions: ['description', 'about', 'contact'],
|
||||
pages: returnPages
|
||||
.sort((a, b) => b.ratio - a.ratio)
|
||||
.slice(0, 15)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private async getPages(): Promise<Page[]> {
|
||||
return await Pages.getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of pages with a given words
|
||||
* @param words
|
||||
* @private
|
||||
*/
|
||||
private async getPagesByWords(words: string[]) {
|
||||
const pagesList: {[key: string]: number} = {};
|
||||
|
||||
/**
|
||||
* Get list of words starting with a words from the search query
|
||||
*/
|
||||
const validWords = Object.keys(globalWords)
|
||||
.filter(word => {
|
||||
return !!words.filter(searchWord => word.indexOf(searchWord) !== -1).length
|
||||
});
|
||||
|
||||
/**
|
||||
* For each word get list of pages with this word
|
||||
*/
|
||||
validWords.forEach(word => {
|
||||
Object.keys(globalWords[word])
|
||||
.forEach(pageId => {
|
||||
if (!pagesList[pageId]) {
|
||||
pagesList[pageId] = 0;
|
||||
}
|
||||
|
||||
pagesList[pageId] += globalWords[word][pageId]
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Sort pages by frequency of given words
|
||||
*/
|
||||
const sortedPagesList = Object.keys(pagesList)
|
||||
.map(pageId => {
|
||||
return {
|
||||
pageId,
|
||||
ratio: pagesList[pageId]
|
||||
}
|
||||
})
|
||||
.sort((a, b) => b.ratio - a.ratio);
|
||||
|
||||
return sortedPagesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get block's ratio. It is used to calculate the weight of the words in the block
|
||||
* @param block
|
||||
* @private
|
||||
*/
|
||||
private getBlockRatio(block: any) {
|
||||
switch (block.type) {
|
||||
case 'header':
|
||||
if (block.data.level === 1) {
|
||||
return 16;
|
||||
} else {
|
||||
return 2;
|
||||
}
|
||||
|
||||
case 'paragraph':
|
||||
return 1.1;
|
||||
|
||||
case 'list':
|
||||
return 1;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return clear text content from block without HTML tags and special characters
|
||||
* @param block
|
||||
* @private
|
||||
*/
|
||||
private getCleanTextFromBlock(block: any): string {
|
||||
let blockContent = '';
|
||||
|
||||
switch (block.type) {
|
||||
case 'header':
|
||||
blockContent = block.data.text;
|
||||
break;
|
||||
|
||||
case 'paragraph':
|
||||
blockContent = block.data.text
|
||||
break;
|
||||
|
||||
case 'list':
|
||||
blockContent = block.data.items.join(' ');
|
||||
break;
|
||||
|
||||
default:
|
||||
return blockContent;
|
||||
}
|
||||
|
||||
blockContent = this.removeHTMLTags(blockContent);
|
||||
blockContent = this.removeHTMLSpecialCharacters(blockContent);
|
||||
|
||||
return blockContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove HTML tags from string. Only content inside tags will be left
|
||||
* @param text
|
||||
* @private
|
||||
*/
|
||||
private removeHTMLTags(text: string) {
|
||||
return text.replace(/<[^>]*>?/gm, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove special characters from text. For example: & " < >
|
||||
* @param text
|
||||
* @private
|
||||
*/
|
||||
private removeHTMLSpecialCharacters(text: string) {
|
||||
return text.replace(/&[^;]*;?/gm, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Split text to words
|
||||
* @param text
|
||||
* @private
|
||||
*/
|
||||
private splitTextToWords(text: string): string[] {
|
||||
return text
|
||||
// lowercase all words
|
||||
.toLowerCase()
|
||||
|
||||
// remove punctuation
|
||||
.replace(/[.,;:]/gi, '')
|
||||
|
||||
// left only letters (+cyrillic) and numbers
|
||||
.replace(/[^a-zа-я0-9]/gi, ' ')
|
||||
|
||||
// remove multiple spaces
|
||||
.replace(/\s+/g, ' ')
|
||||
|
||||
// remove spaces at the beginning and at the end
|
||||
.trim()
|
||||
|
||||
// split to words by spaces
|
||||
.split(' ')
|
||||
|
||||
// ignore words shorter than 3 chars
|
||||
.filter(word => word.length >= 3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight substring in string with a span wrapper
|
||||
*/
|
||||
private highlightSubstring(text: string, words: string|string[]) {
|
||||
if (typeof words === 'string') {
|
||||
words = [words];
|
||||
}
|
||||
|
||||
const wordRegExp = new RegExp(words.join('|'), "ig");
|
||||
const CLASS_STYLE = 'search-word';
|
||||
|
||||
return text.replace(wordRegExp, `<span class="${CLASS_STYLE}">$&</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export initialized instance
|
||||
*/
|
||||
export default new Search();
|
|
@ -1,12 +1,15 @@
|
|||
import express from 'express';
|
||||
|
||||
import pagesAPI from './pages.js';
|
||||
import transportAPI from './transport.js';
|
||||
import linksAPI from './links.js';
|
||||
import searchAPI from './search.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.use('/', pagesAPI);
|
||||
router.use('/', transportAPI);
|
||||
router.use('/', linksAPI);
|
||||
router.use('/', searchAPI);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -2,6 +2,7 @@ import express, { Request, Response } from 'express';
|
|||
import multerFunc from 'multer';
|
||||
import Pages from '../../controllers/pages.js';
|
||||
import PagesOrder from '../../controllers/pagesOrder.js';
|
||||
import Search from '../../controllers/search.js';
|
||||
|
||||
const router = express.Router();
|
||||
const multer = multerFunc();
|
||||
|
@ -70,6 +71,9 @@ router.put('/page', multer.none(), async (req: Request, res: Response) => {
|
|||
/** push to the orders array */
|
||||
await PagesOrder.push(parent, page._id);
|
||||
|
||||
/** Update search index */
|
||||
await Search.syncDB();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: page,
|
||||
|
@ -127,6 +131,10 @@ router.post('/page/:id', multer.none(), async (req: Request, res: Response) => {
|
|||
parent,
|
||||
uri,
|
||||
});
|
||||
|
||||
/** Update search index */
|
||||
await Search.syncDB();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: page,
|
||||
|
@ -206,6 +214,9 @@ router.delete('/page/:id', async (req: Request, res: Response) => {
|
|||
parentPageOrder.remove(req.params.id);
|
||||
await parentPageOrder.save();
|
||||
|
||||
/** Update search index */
|
||||
await Search.syncDB();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: pageToRedirect,
|
||||
|
|
66
src/backend/routes/api/search.ts
Normal file
66
src/backend/routes/api/search.ts
Normal file
|
@ -0,0 +1,66 @@
|
|||
import express, { Request, Response } from 'express';
|
||||
import Search from '../../controllers/search.js';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /search/:searchString
|
||||
*
|
||||
* Search given words in all documents
|
||||
*/
|
||||
router.get('/search', async (req: Request, res: Response) => {
|
||||
try {
|
||||
/**
|
||||
* Get search string
|
||||
*/
|
||||
const searchString = req.query.text as string;
|
||||
|
||||
/**
|
||||
* Get search results
|
||||
*/
|
||||
const searchResponse = await Search.query(searchString);
|
||||
|
||||
/**
|
||||
* Compose response
|
||||
*/
|
||||
const compactedPages = searchResponse.pages.map(page => {
|
||||
return {
|
||||
/** Page id */
|
||||
_id: page._id,
|
||||
|
||||
/** Page title */
|
||||
title: page.title,
|
||||
|
||||
/** Page uri */
|
||||
uri: page.uri,
|
||||
|
||||
/** Section heading name for the found fragment */
|
||||
section: page.section,
|
||||
|
||||
/** Section's anchor */
|
||||
anchor: page.anchor,
|
||||
|
||||
/** Page fragment with searched items */
|
||||
shortBody: page.shortBody,
|
||||
};
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
/** Found pages */
|
||||
pages: compactedPages,
|
||||
|
||||
/** Typing suggestions */
|
||||
suggestions: searchResponse.suggestions,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: (err as Error).message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -15,7 +15,7 @@
|
|||
hawkClientToken:"{{ config.hawkClientToken }}",
|
||||
};
|
||||
</script>
|
||||
<body>
|
||||
<body data-module="search">
|
||||
{% include "components/header.twig" with res.locals.isAuthorized %}
|
||||
<div class="docs">
|
||||
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
{% include 'components/button.twig' with {label: 'Edit', icon: 'pencil', size: 'small', url: '/page/edit/' ~ page._id, class: 'page__header-button'} %}
|
||||
{% endif %}
|
||||
</header>
|
||||
<h1 class="page__title">
|
||||
<h1 class="page__title" id="{{ page.title | urlify }}">
|
||||
{{ page.title }}
|
||||
</h1>
|
||||
{% if (config.carbon and config.carbon.placement and config.carbon.serve) %}
|
||||
|
|
|
@ -17,6 +17,7 @@ import Writing from './modules/writing';
|
|||
import Page from './modules/page';
|
||||
import Extensions from './modules/extensions';
|
||||
import Sidebar from './modules/sidebar';
|
||||
import Search from './modules/search';
|
||||
import HawkCatcher from '@hawk.so/javascript';
|
||||
|
||||
/**
|
||||
|
@ -31,6 +32,8 @@ class Docs {
|
|||
this.page = new Page();
|
||||
this.extensions = new Extensions();
|
||||
this.sidebar = new Sidebar();
|
||||
this.search = new Search();
|
||||
|
||||
if (window.config.hawkClientToken) {
|
||||
this.hawk = new HawkCatcher(window.config.hawkClientToken);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ export default class Page {
|
|||
init() {
|
||||
this.codeStyler = this.createCodeStyling();
|
||||
this.tableOfContent = this.createTableOfContent();
|
||||
this.search = this.createSearch();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
238
src/frontend/js/modules/search.js
Normal file
238
src/frontend/js/modules/search.js
Normal file
|
@ -0,0 +1,238 @@
|
|||
import { debounce } from '../utils/decorators';
|
||||
import Shortcut from '@codexteam/shortcuts';
|
||||
import axios from 'axios';
|
||||
|
||||
export default class Search {
|
||||
constructor() {
|
||||
this.nodes = {
|
||||
overlay: null,
|
||||
searchWrapper: null,
|
||||
searchInput: null,
|
||||
searchResultWrapper: null
|
||||
};
|
||||
|
||||
this.isVisible = false;
|
||||
|
||||
this.PLACEHOLDER = 'Search docs';
|
||||
|
||||
this.TOGGLER_SHORTCUT = 'CMD+SHIFT+F';
|
||||
this.shortcut = null;
|
||||
|
||||
this.DEBOUNCE_TIME = 300;
|
||||
this.debouncedSearch = null;
|
||||
|
||||
this.MIN_SEARCH_LENGTH = 1;
|
||||
|
||||
this.CSS = {
|
||||
overlay: 'search-overlay',
|
||||
overlayVisible: 'search-overlay--visible',
|
||||
searchWrapper: 'search-wrapper',
|
||||
searchInput: 'search-input',
|
||||
searchResultWrapper: 'search-result-wrapper',
|
||||
|
||||
searchResultSuggestions: 'search-result-suggestions',
|
||||
searchResultSuggestionItem: 'search-result-suggestions-item',
|
||||
|
||||
searchResultItem: 'search-result-item',
|
||||
searchResultItemTitle: 'search-result-item__title',
|
||||
searchResultItemSection: 'search-result-item__section',
|
||||
searchResultItemDescription: 'search-result-item__description',
|
||||
|
||||
blurred: 'blurred',
|
||||
noscroll: 'noscroll'
|
||||
};
|
||||
}
|
||||
|
||||
init(settings = {}, moduleEl) {
|
||||
console.log('search init');
|
||||
|
||||
this.createSearchOverlay();
|
||||
this.createDebouncedSearch();
|
||||
this.enableShortcutListening();
|
||||
|
||||
/**
|
||||
* Only for development needs:
|
||||
*
|
||||
* - force open overlay
|
||||
* - type a search string
|
||||
* - fire search
|
||||
*/
|
||||
// const testString = 'codex descri';
|
||||
// this.toggleSearchOverlay(true);
|
||||
// this.nodes.searchInput.value = testString;
|
||||
// this.debouncedSearch(testString);
|
||||
}
|
||||
|
||||
createSearchOverlay() {
|
||||
this.nodes.overlay = document.createElement('div');
|
||||
this.nodes.overlay.classList.add(this.CSS.overlay);
|
||||
this.nodes.overlay.addEventListener('click', this.searchOverlayClickProcessor.bind(this));
|
||||
|
||||
this.nodes.searchWrapper = document.createElement('div');
|
||||
this.nodes.searchWrapper.classList.add(this.CSS.searchWrapper);
|
||||
|
||||
this.nodes.searchInput = document.createElement('input');
|
||||
this.nodes.searchInput.classList.add(this.CSS.searchInput);
|
||||
this.nodes.searchInput.setAttribute('type', 'search');
|
||||
this.nodes.searchInput.setAttribute('placeholder', this.PLACEHOLDER);
|
||||
this.nodes.searchInput.setAttribute('autocomplete', 'off');
|
||||
this.nodes.searchInput.addEventListener('input', this.searchInputOnchangeProcessor.bind(this));
|
||||
this.nodes.searchWrapper.appendChild(this.nodes.searchInput);
|
||||
|
||||
this.nodes.searchResultWrapper = document.createElement('div');
|
||||
this.nodes.searchResultWrapper.classList.add(this.CSS.searchResultWrapper);
|
||||
this.nodes.searchWrapper.appendChild(this.nodes.searchResultWrapper);
|
||||
|
||||
this.nodes.overlay.appendChild(this.nodes.searchWrapper);
|
||||
document.body.appendChild(this.nodes.overlay);
|
||||
}
|
||||
|
||||
searchOverlayClickProcessor(event) {
|
||||
if (event.target !== this.nodes.overlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleSearchOverlay(false);
|
||||
}
|
||||
|
||||
searchInputOnchangeProcessor(event) {
|
||||
const text = event.target.value;
|
||||
|
||||
if (text.length < this.MIN_SEARCH_LENGTH) {
|
||||
this.clearSearchResults();
|
||||
return;
|
||||
}
|
||||
|
||||
this.debouncedSearch(text);
|
||||
}
|
||||
|
||||
enableShortcutListening() {
|
||||
this.shortcut = new Shortcut({
|
||||
name : this.TOGGLER_SHORTCUT,
|
||||
on : document.body,
|
||||
callback: (event) => {
|
||||
this.toggleSearchOverlay();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleSearchOverlay(force) {
|
||||
this.isVisible = force || !this.isVisible;
|
||||
|
||||
this.nodes.overlay.classList.toggle(this.CSS.overlayVisible, this.isVisible);
|
||||
document.body.classList.toggle(this.CSS.noscroll, this.isVisible);
|
||||
|
||||
// blur everything except search overlay
|
||||
try {
|
||||
document.getElementsByClassName('docs-header')[0].classList.toggle(this.CSS.blurred, this.isVisible);
|
||||
document.getElementsByClassName('docs')[0].classList.toggle(this.CSS.blurred, this.isVisible);
|
||||
} catch (e) {}
|
||||
|
||||
this.nodes.searchInput.focus();
|
||||
this.nodes.searchInput.select();
|
||||
}
|
||||
|
||||
createDebouncedSearch() {
|
||||
this.debouncedSearch = debounce(this.getSearchResults, this.DEBOUNCE_TIME);
|
||||
}
|
||||
|
||||
getSearchResults(text) {
|
||||
|
||||
axios.get('/api/search', {
|
||||
params: {
|
||||
text
|
||||
}
|
||||
})
|
||||
.then(this.showSearchResult.bind(this));
|
||||
}
|
||||
|
||||
clearSearchResults() {
|
||||
this.nodes.searchResultWrapper.innerHTML = '';
|
||||
}
|
||||
|
||||
showSearchResult({ data }) {
|
||||
this.clearSearchResults();
|
||||
|
||||
// if (data.result.suggestions.length) {
|
||||
// this.showSuggestedWordCompletion(data.result.suggestions[0]);
|
||||
// }
|
||||
|
||||
// const suggestionsWrapper = this.generateSearchSuggestions(data.result.suggestions);
|
||||
//
|
||||
// this.nodes.searchResultWrapper.appendChild(suggestionsWrapper);
|
||||
|
||||
data.result.pages.forEach(page => {
|
||||
const url = `/${page.uri}` + (page.section ? `#${page.anchor}` : '');
|
||||
|
||||
const result = document.createElement('a');
|
||||
result.classList.add(this.CSS.searchResultItem);
|
||||
result.setAttribute('href', url);
|
||||
result.addEventListener('click', this.toggleSearchOverlay.bind(this, false));
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.classList.add(this.CSS.searchResultItemTitle);
|
||||
title.innerHTML = page.title;
|
||||
result.appendChild(title);
|
||||
|
||||
if (page.section && page.section !== page.title) {
|
||||
const section = document.createElement('span');
|
||||
section.classList.add(this.CSS.searchResultItemSection);
|
||||
section.innerHTML = `${page.section}`;
|
||||
title.appendChild(section);
|
||||
}
|
||||
|
||||
const description = document.createElement('div');
|
||||
description.classList.add(this.CSS.searchResultItemDescription);
|
||||
description.innerHTML = `${page.shortBody}`;
|
||||
result.appendChild(description);
|
||||
|
||||
this.nodes.searchResultWrapper.appendChild(result);
|
||||
});
|
||||
}
|
||||
|
||||
// showSuggestedWordCompletion(word) {
|
||||
// const typedString = this.nodes.searchInput.value;
|
||||
// const words = typedString.split(' ');
|
||||
//
|
||||
// words.pop();
|
||||
// words.push(word);
|
||||
//
|
||||
// this.nodes.searchInput.value = words.join(' ');
|
||||
//
|
||||
// this.nodes.searchInput.select();
|
||||
// this.nodes.searchInput.selectionStart = typedString.length;
|
||||
// this.nodes.searchInput.selectionEnd = this.nodes.searchInput.value.length;
|
||||
// }
|
||||
|
||||
// generateSearchSuggestions(suggestions = []) {
|
||||
// const suggestionsWrapper = document.createElement('div');
|
||||
//
|
||||
// suggestionsWrapper.classList.add(this.CSS.searchResultSuggestions);
|
||||
//
|
||||
// suggestions.forEach(suggestion => {
|
||||
// const suggestionItem = document.createElement('span');
|
||||
//
|
||||
// suggestionItem.classList.add(this.CSS.searchResultSuggestionItem);
|
||||
// suggestionItem.innerHTML = suggestion;
|
||||
// suggestionItem.addEventListener('click', this.searchSuggestionClickProcessor.bind(this));
|
||||
//
|
||||
// suggestionsWrapper.appendChild(suggestionItem);
|
||||
// });
|
||||
//
|
||||
// return suggestionsWrapper;
|
||||
// }
|
||||
//
|
||||
// searchSuggestionClickProcessor(event) {
|
||||
// const word = event.target.innerHTML;
|
||||
//
|
||||
// const searchString = this.nodes.searchInput.value;
|
||||
// const searchStringWords = searchString.split(' ');
|
||||
//
|
||||
// searchStringWords.pop();
|
||||
// searchStringWords.push(word);
|
||||
//
|
||||
// this.nodes.searchInput.value = searchStringWords.join(' ');
|
||||
//
|
||||
// this.debouncedSearch(this.nodes.searchInput.value);
|
||||
// }
|
||||
}
|
119
src/frontend/styles/components/search.pcss
Normal file
119
src/frontend/styles/components/search.pcss
Normal file
|
@ -0,0 +1,119 @@
|
|||
.search {
|
||||
&-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
display: none;
|
||||
overflow-y: scroll;
|
||||
|
||||
z-index: 10;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&--visible {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&-wrapper {
|
||||
background: #fff;
|
||||
margin: 30vh auto 20vh;
|
||||
max-width: var(--layout-width-main-col);
|
||||
width: 100%;
|
||||
|
||||
border-radius: 8px;
|
||||
box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1);
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 100%;
|
||||
background: #bbb;
|
||||
padding: 15px 25px;
|
||||
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
|
||||
&::placeholder {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
&-result {
|
||||
&-wrapper {
|
||||
|
||||
}
|
||||
|
||||
&-suggestions {
|
||||
padding: 5px 25px;
|
||||
overflow-x: scroll;
|
||||
|
||||
&-item {
|
||||
margin-right: 12px;
|
||||
|
||||
padding: 2px 8px;
|
||||
background: #ccedf6;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #b5d8e2;
|
||||
|
||||
&:hover {
|
||||
background: #aad4e0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-item {
|
||||
padding: 15px 25px;
|
||||
display: block;
|
||||
|
||||
&:nth-child(odd) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__section {
|
||||
opacity: 0.7;
|
||||
|
||||
&:before {
|
||||
content: '•';
|
||||
margin: 0 5px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&__description {
|
||||
margin-top: 8px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-word {
|
||||
color: var(--color-link-active);
|
||||
}
|
||||
}
|
||||
|
||||
.noscroll {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.blurred {
|
||||
filter: blur(3px);
|
||||
}
|
||||
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
@import './components/sidebar.pcss';
|
||||
@import './components/navigator.pcss';
|
||||
@import './components/table-of-content.pcss';
|
||||
@import './components/search.pcss';
|
||||
|
||||
body {
|
||||
font-family: system-ui, Helvetica, Arial, Verdana;
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -1732,6 +1732,14 @@ axios@^0.21.1:
|
|||
dependencies:
|
||||
follow-redirects "^1.14.0"
|
||||
|
||||
axios@^0.27.2:
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.27.2.tgz#207658cc8621606e586c85db4b41a750e756d972"
|
||||
integrity sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==
|
||||
dependencies:
|
||||
follow-redirects "^1.14.9"
|
||||
form-data "^4.0.0"
|
||||
|
||||
babel-eslint@^10.0.1:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-eslint/-/babel-eslint-10.1.0.tgz#6968e568a910b78fb3779cdd8b6ac2f479943232"
|
||||
|
@ -2080,7 +2088,8 @@ clone-response@^1.0.2:
|
|||
|
||||
clone@2.x:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==
|
||||
|
||||
code-point-at@^1.0.0:
|
||||
version "1.1.0"
|
||||
|
@ -3162,7 +3171,7 @@ flatten@^1.0.2:
|
|||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.3.tgz#c1283ac9f27b368abc1e36d1ff7b04501a30356b"
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
follow-redirects@^1.14.0, follow-redirects@^1.14.9:
|
||||
version "1.15.1"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.1.tgz#0ca6a452306c9b276e4d3127483e29575e207ad5"
|
||||
|
||||
|
@ -3193,6 +3202,15 @@ form-data@^3.0.0:
|
|||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
form-data@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
|
||||
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
|
||||
dependencies:
|
||||
asynckit "^0.4.0"
|
||||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
formdata-polyfill@^4.0.10:
|
||||
version "4.0.10"
|
||||
resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423"
|
||||
|
@ -4310,7 +4328,8 @@ nise@^5.1.1:
|
|||
|
||||
node-cache@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz"
|
||||
resolved "https://registry.yarnpkg.com/node-cache/-/node-cache-5.1.2.tgz#f264dc2ccad0a780e76253a694e9fd0ed19c398d"
|
||||
integrity sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg==
|
||||
dependencies:
|
||||
clone "2.x"
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue