mirror of
https://github.com/codex-team/codex.docs.git
synced 2025-07-19 13:19:42 +02:00
enable search for cyrillic letters
This commit is contained in:
parent
bab89c979f
commit
cb877398a6
4 changed files with 163 additions and 51 deletions
|
@ -1,12 +1,6 @@
|
||||||
import Page from '../models/page';
|
import Page from '../models/page';
|
||||||
import Pages from '../controllers/pages';
|
import Pages from '../controllers/pages';
|
||||||
|
|
||||||
type SearchResponse = {
|
|
||||||
completions: string[];
|
|
||||||
|
|
||||||
pages: Page[];
|
|
||||||
}
|
|
||||||
|
|
||||||
class Search {
|
class Search {
|
||||||
/**
|
/**
|
||||||
* Prepare words database
|
* Prepare words database
|
||||||
|
@ -23,7 +17,7 @@ class Search {
|
||||||
page.body.blocks.forEach((block: any) => {
|
page.body.blocks.forEach((block: any) => {
|
||||||
let blockContent = '';
|
let blockContent = '';
|
||||||
|
|
||||||
const validBlocks = ['header', 'paragraph'];
|
const validBlocks = ['header', 'paragraph', 'list'];
|
||||||
if (!validBlocks.includes(block.type)) {
|
if (!validBlocks.includes(block.type)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -36,17 +30,21 @@ class Search {
|
||||||
case 'paragraph':
|
case 'paragraph':
|
||||||
blockContent = block.data.text
|
blockContent = block.data.text
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'list':
|
||||||
|
blockContent = block.data.items.join(' ');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const blockWords: string[] = blockContent
|
const blockWords: string[] = blockContent
|
||||||
// @todo get text from inline code elements and remove html tags
|
// @todo get text from inline code elements and remove html tags
|
||||||
|
|
||||||
// left only letters and numbers
|
|
||||||
.replace(/[^a-z0-9]/gi, ' ')
|
|
||||||
|
|
||||||
// lowercase all words
|
// lowercase all words
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
|
|
||||||
|
// left only letters (+cyrillic) and numbers
|
||||||
|
.replace(/[^a-zа-я0-9]/gi, ' ')
|
||||||
|
|
||||||
// remove multiple spaces
|
// remove multiple spaces
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ class Search {
|
||||||
return pagesWords;
|
return pagesWords;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async query(searchString: string): Promise<SearchResponse> {
|
public async query(searchString: string) {
|
||||||
const pages = await Pages.getAll();
|
const pages = await Pages.getAll();
|
||||||
const pagesWords = await this.index();
|
const pagesWords = await this.index();
|
||||||
|
|
||||||
|
@ -95,10 +93,47 @@ class Search {
|
||||||
});
|
});
|
||||||
|
|
||||||
const foundPages = goodPages
|
const foundPages = goodPages
|
||||||
.filter(({ successRatio }) => successRatio > 50)
|
.filter(({ successRatio }) => successRatio > 75)
|
||||||
.sort((a, b) => b.successRatio - a.successRatio)
|
.sort((a, b) => b.successRatio - a.successRatio)
|
||||||
.slice(0, 10);
|
.slice(0, 10);
|
||||||
|
|
||||||
|
const returnPages = pages.filter(page => foundPages.some(({ id }) => id === page._id))
|
||||||
|
.map(page => {
|
||||||
|
let shortBody = '...';
|
||||||
|
let score = 1;
|
||||||
|
|
||||||
|
page.body.blocks.forEach((block: any) => {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchWords.forEach(word => {
|
||||||
|
blockContent = blockContent.replace(word, `<span class="search-word">${word}</span>`);
|
||||||
|
})
|
||||||
|
|
||||||
|
// shortBody += blockContent;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...page,
|
||||||
|
shortBody
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --------- START test ---------
|
// --------- START test ---------
|
||||||
|
|
||||||
|
@ -113,8 +148,8 @@ class Search {
|
||||||
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
completions: uniqWords.filter(word => word.indexOf(searchWords.slice(-1)[0]) === 0),
|
suggestions: uniqWords.filter(word => word.indexOf(searchWords.slice(-1)[0]) === 0),
|
||||||
pages: pages.filter(page => foundPages.some(({ id }) => id === page._id))
|
pages: returnPages
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,14 @@ router.get('/search', async (req: Request, res: Response) => {
|
||||||
uri: page.uri,
|
uri: page.uri,
|
||||||
// body: page.body,
|
// body: page.body,
|
||||||
// parent: page.parent,
|
// parent: page.parent,
|
||||||
|
shortBody: page.shortBody,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
completions: searchResponse.completions,
|
suggestions: searchResponse.suggestions,
|
||||||
pages: compactedPages,
|
pages: compactedPages,
|
||||||
time: searchItem,
|
time: searchItem,
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,23 +2,44 @@ import { debounce } from '../utils/decorators';
|
||||||
import Shortcut from '@codexteam/shortcuts';
|
import Shortcut from '@codexteam/shortcuts';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
|
||||||
export default class Search {
|
export default class Search {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.nodes = {
|
this.nodes = {
|
||||||
overlay: null,
|
overlay: null,
|
||||||
searchWrapper: null,
|
searchWrapper: null,
|
||||||
searchInput: null,
|
searchInput: null,
|
||||||
searchResultsWrapper: null
|
searchResultWrapper: null
|
||||||
};
|
};
|
||||||
|
|
||||||
this.isVisible = false;
|
this.isVisible = false;
|
||||||
|
|
||||||
this.shortcut = null;
|
this.PLACEHOLDER = 'Find in documents...';
|
||||||
this.TOGGLER_SHORTCUT = 'CMD+SHIFT+F';
|
|
||||||
|
|
||||||
|
this.TOGGLER_SHORTCUT = 'CMD+SHIFT+F';
|
||||||
|
this.shortcut = null;
|
||||||
|
|
||||||
|
this.DEBOUNCE_TIME = 300;
|
||||||
this.debouncedSearch = null;
|
this.debouncedSearch = null;
|
||||||
this.DEBOUNCE_TIME = 500;
|
|
||||||
|
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',
|
||||||
|
searchResultItemDescription: 'search-result-item__description',
|
||||||
|
|
||||||
|
blur: 'blurred',
|
||||||
|
noscroll: 'noscroll'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
init(settings = {}, moduleEl) {
|
init(settings = {}, moduleEl) {
|
||||||
|
@ -30,27 +51,30 @@ export default class Search {
|
||||||
|
|
||||||
// ! force open search overlay
|
// ! force open search overlay
|
||||||
// this.toggleSearchOverlay(true);
|
// this.toggleSearchOverlay(true);
|
||||||
|
// const testString = 'api';
|
||||||
|
// this.nodes.searchInput.value = testString;
|
||||||
|
// this.debouncedSearch(testString);
|
||||||
}
|
}
|
||||||
|
|
||||||
createSearchOverlay() {
|
createSearchOverlay() {
|
||||||
this.nodes.overlay = document.createElement('div');
|
this.nodes.overlay = document.createElement('div');
|
||||||
this.nodes.overlay.classList.add('search-overlay');
|
this.nodes.overlay.classList.add(this.CSS.overlay);
|
||||||
this.nodes.overlay.addEventListener('click', this.searchOverlayClickProcessor.bind(this));
|
this.nodes.overlay.addEventListener('click', this.searchOverlayClickProcessor.bind(this));
|
||||||
|
|
||||||
this.nodes.searchWrapper = document.createElement('div');
|
this.nodes.searchWrapper = document.createElement('div');
|
||||||
this.nodes.searchWrapper.classList.add('search-wrapper');
|
this.nodes.searchWrapper.classList.add(this.CSS.searchWrapper);
|
||||||
|
|
||||||
this.nodes.searchInput = document.createElement('input');
|
this.nodes.searchInput = document.createElement('input');
|
||||||
this.nodes.searchInput.classList.add('search-input');
|
this.nodes.searchInput.classList.add(this.CSS.searchInput);
|
||||||
this.nodes.searchInput.setAttribute('type', 'search');
|
this.nodes.searchInput.setAttribute('type', 'search');
|
||||||
this.nodes.searchInput.setAttribute('placeholder', 'Find in documents...');
|
this.nodes.searchInput.setAttribute('placeholder', this.PLACEHOLDER);
|
||||||
this.nodes.searchInput.setAttribute('autocomplete', 'off');
|
this.nodes.searchInput.setAttribute('autocomplete', 'off');
|
||||||
this.nodes.searchInput.addEventListener('input', this.searchInputOnchangeProcessor.bind(this));
|
this.nodes.searchInput.addEventListener('input', this.searchInputOnchangeProcessor.bind(this));
|
||||||
this.nodes.searchWrapper.appendChild(this.nodes.searchInput);
|
this.nodes.searchWrapper.appendChild(this.nodes.searchInput);
|
||||||
|
|
||||||
this.nodes.searchResultsWrapper = document.createElement('div');
|
this.nodes.searchResultWrapper = document.createElement('div');
|
||||||
this.nodes.searchResultsWrapper.classList.add('search-results-wrapper');
|
this.nodes.searchResultWrapper.classList.add(this.CSS.searchResultWrapper);
|
||||||
this.nodes.searchWrapper.appendChild(this.nodes.searchResultsWrapper);
|
this.nodes.searchWrapper.appendChild(this.nodes.searchResultWrapper);
|
||||||
|
|
||||||
this.nodes.overlay.appendChild(this.nodes.searchWrapper);
|
this.nodes.overlay.appendChild(this.nodes.searchWrapper);
|
||||||
document.body.appendChild(this.nodes.overlay);
|
document.body.appendChild(this.nodes.overlay);
|
||||||
|
@ -65,15 +89,14 @@ export default class Search {
|
||||||
}
|
}
|
||||||
|
|
||||||
searchInputOnchangeProcessor(event) {
|
searchInputOnchangeProcessor(event) {
|
||||||
// close search overlay if ESC key is pressed
|
const text = event.target.value;
|
||||||
if (event.keyCode === 27) {
|
|
||||||
this.toggleSearchOverlay(false);
|
if (text.length < this.MIN_SEARCH_LENGTH) {
|
||||||
event.preventDefault();
|
this.clearSearchResults();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(event.target.value);
|
this.debouncedSearch(text);
|
||||||
|
|
||||||
this.debouncedSearch(event.target.value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enableShortcutListening() {
|
enableShortcutListening() {
|
||||||
|
@ -89,12 +112,13 @@ export default class Search {
|
||||||
toggleSearchOverlay(force) {
|
toggleSearchOverlay(force) {
|
||||||
this.isVisible = force || !this.isVisible;
|
this.isVisible = force || !this.isVisible;
|
||||||
|
|
||||||
this.nodes.overlay.classList.toggle('search-overlay--visible', this.isVisible);
|
this.nodes.overlay.classList.toggle(this.CSS.overlayVisible, this.isVisible);
|
||||||
document.body.classList.toggle('noscroll', this.isVisible);
|
document.body.classList.toggle(this.CSS.noscroll, this.isVisible);
|
||||||
|
|
||||||
|
// blur everything except search overlay
|
||||||
try {
|
try {
|
||||||
document.getElementsByClassName('docs-header')[0].classList.toggle('blurred', this.isVisible);
|
document.getElementsByClassName('docs-header')[0].classList.toggle(this.CSS.blurred, this.isVisible);
|
||||||
document.getElementsByClassName('docs')[0].classList.toggle('blurred', this.isVisible);
|
document.getElementsByClassName('docs')[0].classList.toggle(this.CSS.blurred, this.isVisible);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
this.nodes.searchInput.focus();
|
this.nodes.searchInput.focus();
|
||||||
|
@ -105,41 +129,74 @@ export default class Search {
|
||||||
}
|
}
|
||||||
|
|
||||||
getSearchResults(text) {
|
getSearchResults(text) {
|
||||||
if (!text) {
|
|
||||||
this.clearSearchResults();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
axios.get('/api/search', {
|
axios.get('/api/search', {
|
||||||
params: {
|
params: {
|
||||||
text: text
|
text
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then(this.showSearchResult.bind(this));
|
.then(this.showSearchResult.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
clearSearchResults() {
|
clearSearchResults() {
|
||||||
this.nodes.searchResultsWrapper.innerHTML = '';
|
this.nodes.searchResultWrapper.innerHTML = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
showSearchResult({ data }) {
|
showSearchResult({ data }) {
|
||||||
this.clearSearchResults();
|
this.clearSearchResults();
|
||||||
|
|
||||||
|
// const suggestionsWrapper = this.generateSearchSuggestions(data.result.suggestions);
|
||||||
|
//
|
||||||
|
// this.nodes.searchResultWrapper.appendChild(suggestionsWrapper);
|
||||||
|
|
||||||
data.result.pages.forEach(page => {
|
data.result.pages.forEach(page => {
|
||||||
const result = document.createElement('a');
|
const result = document.createElement('a');
|
||||||
result.classList.add('search-results-item');
|
result.classList.add(this.CSS.searchResultItem);
|
||||||
result.setAttribute('href', `/${page.uri}`);
|
result.setAttribute('href', `/${page.uri}`);
|
||||||
|
|
||||||
const title = document.createElement('div');
|
const title = document.createElement('div');
|
||||||
title.classList.add('search-results-item__title');
|
title.classList.add(this.CSS.searchResultItemTitle);
|
||||||
title.innerHTML = page.title;
|
title.innerHTML = page.title;
|
||||||
result.appendChild(title);
|
result.appendChild(title);
|
||||||
|
|
||||||
// const description = document.createElement('div');
|
const description = document.createElement('div');
|
||||||
// description.classList.add('search-results-item__description');
|
description.classList.add(this.CSS.searchResultItemDescription);
|
||||||
|
description.innerHTML = `${page.shortBody}`;
|
||||||
// result.appendChild(description);
|
// result.appendChild(description);
|
||||||
|
|
||||||
this.nodes.searchResultsWrapper.appendChild(result);
|
this.nodes.searchResultWrapper.appendChild(result);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,11 +44,29 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&-results {
|
&-result {
|
||||||
&-wrapper {
|
&-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 {
|
&-item {
|
||||||
padding: 15px 25px;
|
padding: 15px 25px;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -66,6 +84,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__description {
|
&__description {
|
||||||
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +96,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.blurred {
|
.blurred {
|
||||||
filter: blur(2px);
|
filter: blur(3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue