mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-23 03:42:27 +03:00
🌐 Added ⬅️RTL to sodo-search & improved tests (#21152)
no ref - added dir prop, calculated by i18next from language (using the dir function) - tweaked a few styles to use me/ms/pe/ps instead of mr/ml/pr/pl - added updated test that checks that stemming works in English, and added tests for partial and full-word searching with RTL content.
This commit is contained in:
parent
300eba49ca
commit
6e599ef541
@ -9,20 +9,23 @@ export default class App extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const searchIndex = new SearchIndex({
|
||||
adminUrl: props.adminUrl,
|
||||
apiKey: props.apiKey
|
||||
});
|
||||
|
||||
const i18nLanguage = this.props.locale || 'en';
|
||||
const i18n = i18nLib(i18nLanguage, 'search');
|
||||
const dir = i18n.dir() || 'ltr';
|
||||
|
||||
const searchIndex = new SearchIndex({
|
||||
adminUrl: props.adminUrl,
|
||||
apiKey: props.apiKey,
|
||||
dir: dir
|
||||
});
|
||||
|
||||
this.state = {
|
||||
searchIndex,
|
||||
showPopup: false,
|
||||
indexStarted: false,
|
||||
indexComplete: false,
|
||||
t: i18n.t
|
||||
t: i18n.t,
|
||||
dir: dir
|
||||
};
|
||||
|
||||
this.inputRef = React.createRef();
|
||||
@ -169,7 +172,8 @@ export default class App extends React.Component {
|
||||
});
|
||||
}
|
||||
},
|
||||
t: this.state.t
|
||||
t: this.state.t,
|
||||
dir: this.state.dir
|
||||
}}>
|
||||
<PopupModal />
|
||||
</AppContext.Provider>
|
||||
|
@ -14,7 +14,8 @@ const AppContext = React.createContext({
|
||||
searchIndex: null,
|
||||
indexComplete: false,
|
||||
searchValue: '',
|
||||
t: () => {}
|
||||
t: () => {},
|
||||
dir: 'ltr'
|
||||
});
|
||||
|
||||
export default AppContext;
|
||||
|
@ -17,6 +17,8 @@ export default class Frame extends Component {
|
||||
setupFrameBaseStyle() {
|
||||
if (this.node.contentDocument) {
|
||||
this.iframeHtml = this.node.contentDocument.documentElement;
|
||||
// set the iframeHtml dir attribute
|
||||
this.iframeHtml.setAttribute('dir', this.props.searchdir);
|
||||
this.iframeHead = this.node.contentDocument.head;
|
||||
this.iframeRoot = this.node.contentDocument.body;
|
||||
this.forceUpdate();
|
||||
|
@ -100,7 +100,7 @@ function SearchBox() {
|
||||
|
||||
return (
|
||||
<div className={className} ref={containerRef}>
|
||||
<div className='flex items-center justify-center w-4 h-4 mr-3'>
|
||||
<div className='flex items-center justify-center w-4 h-4 me-3'>
|
||||
<SearchClearIcon />
|
||||
</div>
|
||||
<input
|
||||
@ -116,7 +116,7 @@ function SearchBox() {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
className='grow -my-5 py-5 -ml-3 pl-3 text-[1.65rem] focus-visible:outline-none placeholder:text-gray-400 outline-none truncate'
|
||||
className='grow -my-5 py-5 -ms-3 ps-3 text-[1.65rem] focus-visible:outline-none placeholder:text-gray-400 outline-none truncate'
|
||||
placeholder={t('Search posts, tags and authors')}
|
||||
/>
|
||||
<Loading />
|
||||
@ -158,7 +158,7 @@ function CancelButton() {
|
||||
|
||||
return (
|
||||
<button
|
||||
className='ml-3 text-sm text-neutral-500 sm:hidden' alt='Cancel'
|
||||
className='ms-3 text-sm text-neutral-500 sm:hidden' alt='Cancel'
|
||||
onClick={() => {
|
||||
dispatch('update', {
|
||||
showPopup: false
|
||||
@ -188,7 +188,7 @@ function TagListItem({tag, selectedResult, setSelectedResult}) {
|
||||
setSelectedResult(id);
|
||||
}}
|
||||
>
|
||||
<p className='mr-2 text-sm font-bold text-neutral-400'>#</p>
|
||||
<p className='me-2 text-sm font-bold text-neutral-400'>#</p>
|
||||
<h2 className='text-[1.65rem] font-medium leading-tight text-neutral-900 truncate'>{name}</h2>
|
||||
</div>
|
||||
);
|
||||
@ -433,11 +433,11 @@ function AuthorAvatar({name, avatar}) {
|
||||
const Character = name.charAt(0);
|
||||
if (Avatar) {
|
||||
return (
|
||||
<img className='rounded-full bg-neutral-300 w-7 h-7 mr-2 object-cover' src={avatar} alt={name}/>
|
||||
<img className='rounded-full bg-neutral-300 w-7 h-7 me-2 object-cover' src={avatar} alt={name}/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='rounded-full bg-neutral-200 w-7 h-7 mr-2 flex items-center justify-center font-bold'><span className="text-neutral-400">{Character}</span></div>
|
||||
<div className='rounded-full bg-neutral-200 w-7 h-7 me-2 flex items-center justify-center font-bold'><span className="text-neutral-400">{Character}</span></div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -672,7 +672,7 @@ export default class PopupModal extends React.Component {
|
||||
|
||||
return (
|
||||
<div style={Styles.modalContainer} className='gh-root-frame'>
|
||||
<Frame style={frameStyle} title='portal-popup' head={this.renderFrameStyles()}>
|
||||
<Frame style={frameStyle} title='portal-popup' head={this.renderFrameStyles()} searchdir={this.context.dir}>
|
||||
<div
|
||||
onClick = {e => this.handlePopupClose(e)}
|
||||
className='absolute top-0 bottom-0 left-0 right-0 block backdrop-blur-[2px] animate-fadein z-0 bg-gradient-to-br from-[rgba(0,0,0,0.2)] to-[rgba(0,0,0,0.1)]' />
|
||||
|
@ -2,15 +2,17 @@ import Flexsearch from 'flexsearch';
|
||||
import GhostContentAPI from '@tryghost/content-api';
|
||||
|
||||
export default class SearchIndex {
|
||||
constructor({adminUrl, apiKey}) {
|
||||
constructor({adminUrl, apiKey, dir}) {
|
||||
this.api = new GhostContentAPI({
|
||||
url: adminUrl,
|
||||
key: apiKey,
|
||||
version: 'v5.0'
|
||||
});
|
||||
|
||||
const rtl = (dir === 'rtl');
|
||||
const tokenize = (dir === 'rtl') ? 'reverse' : 'forward';
|
||||
this.postsIndex = new Flexsearch.Document({
|
||||
tokenize: 'forward',
|
||||
tokenize: tokenize,
|
||||
rtl: rtl,
|
||||
document: {
|
||||
id: 'id',
|
||||
index: ['title', 'excerpt'],
|
||||
@ -19,7 +21,8 @@ export default class SearchIndex {
|
||||
...this.#getEncodeOptions()
|
||||
});
|
||||
this.authorsIndex = new Flexsearch.Document({
|
||||
tokenize: 'forward',
|
||||
tokenize: tokenize,
|
||||
rtl: rtl,
|
||||
document: {
|
||||
id: 'id',
|
||||
index: ['name'],
|
||||
@ -28,7 +31,8 @@ export default class SearchIndex {
|
||||
...this.#getEncodeOptions()
|
||||
});
|
||||
this.tagsIndex = new Flexsearch.Document({
|
||||
tokenize: 'forward',
|
||||
tokenize: tokenize,
|
||||
rtl: rtl,
|
||||
document: {
|
||||
id: 'id',
|
||||
index: ['name'],
|
||||
|
@ -72,7 +72,7 @@ describe('search index', function () {
|
||||
url: 'http://localhost/ghost/tags/barcelona-tag/'
|
||||
}]
|
||||
});
|
||||
|
||||
|
||||
await searchIndex.init();
|
||||
|
||||
let searchResults = searchIndex.search('Barcelo');
|
||||
@ -93,5 +93,164 @@ describe('search index', function () {
|
||||
expect(searchResults.posts.length).toEqual(0);
|
||||
expect(searchResults.authors.length).toEqual(0);
|
||||
expect(searchResults.tags.length).toEqual(0);
|
||||
|
||||
// confirms that search works in the forward direction for ltr languages:
|
||||
let searchWithStartResults = searchIndex.search('Barce');
|
||||
expect(searchWithStartResults.posts.length).toEqual(1);
|
||||
|
||||
let searchWithEndResults = searchIndex.search('celona');
|
||||
expect(searchWithEndResults.posts.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('searching works when dir = rtl also', async () => {
|
||||
const adminUrl = 'http://localhost:3000';
|
||||
const apiKey = '69010382388f9de5869ad6e558';
|
||||
const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage});
|
||||
|
||||
nock('http://localhost:3000/ghost/api/content')
|
||||
.get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC')
|
||||
.reply(200, {
|
||||
posts: [{
|
||||
id: 'sounique',
|
||||
title: 'أُظهر المثابرة كل يوم',
|
||||
excerpt: 'أظهر المثابرة كل يوم. كتابة الاختبارات تحدٍ كبير!',
|
||||
url: 'http://localhost/ghost/awesome-barcelona-life/'
|
||||
},
|
||||
{
|
||||
id: 'sounique2',
|
||||
title: 'هذا منشور عن السعادة',
|
||||
excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.',
|
||||
url: 'http://localhost/ghost/awesome-barcelona-life2/'
|
||||
}]
|
||||
})
|
||||
.get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC')
|
||||
.reply(200, {
|
||||
authors: [{
|
||||
id: 'different_uniq',
|
||||
slug: 'barcelona-author',
|
||||
name: 'اسمي المثابرة',
|
||||
profile_image: 'https://url_to_avatar/barcelona.png',
|
||||
url: 'http://localhost/ghost/authors/barcelona-author/'
|
||||
}, {
|
||||
id: 'different_uniq_2',
|
||||
slug: 'bob',
|
||||
name: 'Bob',
|
||||
profile_image: 'https://url_to_avatar/barcelona.png',
|
||||
url: 'http://localhost/ghost/authors/bob/'
|
||||
}]
|
||||
})
|
||||
.get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic')
|
||||
.reply(200, {
|
||||
tags: [{
|
||||
id: 'uniq_tag',
|
||||
slug: 'barcelona-tag',
|
||||
name: 'المثابرة',
|
||||
url: 'http://localhost/ghost/tags/barcelona-tag/'
|
||||
}]
|
||||
});
|
||||
|
||||
await searchIndex.init();
|
||||
|
||||
let searchResults = searchIndex.search('المثابرة');
|
||||
expect(searchResults.posts.length).toEqual(1);
|
||||
expect(searchResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم');
|
||||
expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/awesome-barcelona-life/');
|
||||
|
||||
expect(searchResults.authors.length).toEqual(1);
|
||||
expect(searchResults.authors[0].name).toEqual('اسمي المثابرة');
|
||||
expect(searchResults.authors[0].url).toEqual('http://localhost/ghost/authors/barcelona-author/');
|
||||
expect(searchResults.authors[0].profile_image).toEqual('https://url_to_avatar/barcelona.png');
|
||||
|
||||
expect(searchResults.tags.length).toEqual(1);
|
||||
expect(searchResults.tags[0].name).toEqual('المثابرة');
|
||||
expect(searchResults.tags[0].url).toEqual('http://localhost/ghost/tags/barcelona-tag/');
|
||||
|
||||
searchResults = searchIndex.search('Nothing like this');
|
||||
expect(searchResults.posts.length).toEqual(0);
|
||||
expect(searchResults.authors.length).toEqual(0);
|
||||
expect(searchResults.tags.length).toEqual(0);
|
||||
|
||||
let searchWithStartResults = searchIndex.search('المثا');
|
||||
expect(searchWithStartResults.posts.length).toEqual(1);
|
||||
expect(searchWithStartResults.posts[0].title).toEqual('أُظهر المثابرة كل يوم');
|
||||
|
||||
let searchWithEndResults = searchIndex.search('ثابرة');
|
||||
expect(searchWithEndResults.posts.length).toEqual(0);
|
||||
});
|
||||
|
||||
test('searching handles CJK characters correctly', async () => {
|
||||
const adminUrl = 'http://localhost:3000';
|
||||
const apiKey = '69010382388f9de5869ad6e558';
|
||||
const searchIndex = new SearchIndex({adminUrl, apiKey, dir: 'ltr', storage: localStorage});
|
||||
|
||||
nock('http://localhost:3000/ghost/api/content')
|
||||
.get('/posts/?key=69010382388f9de5869ad6e558&limit=10000&fields=id%2Cslug%2Ctitle%2Cexcerpt%2Curl%2Cupdated_at%2Cvisibility&order=updated_at%20DESC')
|
||||
.reply(200, {
|
||||
posts: [{
|
||||
id: 'sounique',
|
||||
title: '接收電子報 Regisztráljon fizetős',
|
||||
excerpt: '要是系統發送電子報時遇到永久失敗的情形,English 該帳號將停止接收電子報 Regisztráljon fizetős fiókot يتطابق a المثابرة كل يوم hozzásćzólások írásához あなたのリクエストはこのサイトの管理者に送信されます。Пријавете го овој коментар Dołączdo płatnej społeczności {{publication}}, by zaąćcąć komećantować. vietnamese: Yêu cầu nhà cung cấp dịch vụ email hỗ trợ bengali: নিউরো সার্জন',
|
||||
url: 'http://localhost/ghost/visting-china-as-a-polyglot/'
|
||||
},
|
||||
{
|
||||
id: 'sounique2',
|
||||
title: 'هذا منشور عن السعادة',
|
||||
excerpt: 'هذا منشور عن السعادة. لا يتطابق مع استعلام البحث.',
|
||||
url: 'http://localhost/ghost/a-post-in-arabic/'
|
||||
},
|
||||
{
|
||||
id: 'sounique3',
|
||||
title: '毅力和运气',
|
||||
excerpt: '凭借运气和毅力,Cathy 将通过所有测试。',
|
||||
url: 'http://localhost/ghost/a-post-in-chinese/'
|
||||
}]
|
||||
})
|
||||
.get('/authors/?key=69010382388f9de5869ad6e558&limit=10000&fields=id,slug,name,url,profile_image&order=updated_at%20DESC')
|
||||
.reply(200, {
|
||||
authors: [{
|
||||
id: 'different_uniq',
|
||||
slug: 'barcelona-author',
|
||||
name: 'Barcelona Author',
|
||||
profile_image: 'https://url_to_avatar/barcelona.png',
|
||||
url: 'http://localhost/ghost/authors/barcelona-author/'
|
||||
}, {
|
||||
id: 'different_uniq_2',
|
||||
slug: 'bob',
|
||||
name: 'Bob',
|
||||
profile_image: 'https://url_to_avatar/barcelona.png',
|
||||
url: 'http://localhost/ghost/authors/bob/'
|
||||
}]
|
||||
})
|
||||
.get('/tags/?key=69010382388f9de5869ad6e558&&limit=10000&fields=id,slug,name,url&order=updated_at%20DESC&filter=visibility%3Apublic')
|
||||
.reply(200, {
|
||||
tags: [{
|
||||
id: 'uniq_tag',
|
||||
slug: 'barcelona-tag',
|
||||
name: 'Barcelona Tag',
|
||||
url: 'http://localhost/ghost/tags/barcelona-tag/'
|
||||
}]
|
||||
});
|
||||
|
||||
await searchIndex.init();
|
||||
|
||||
let searchResults = searchIndex.search('Regisztrálj');
|
||||
expect(searchResults.posts.length).toEqual(1);
|
||||
expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/');
|
||||
|
||||
searchResults = searchIndex.search('Nothing like this');
|
||||
expect(searchResults.posts.length).toEqual(0);
|
||||
|
||||
searchResults = searchIndex.search('報');
|
||||
expect(searchResults.posts.length).toEqual(1);
|
||||
expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/');
|
||||
|
||||
// out of order Chinese:
|
||||
searchResults = searchIndex.search('接子收電');
|
||||
expect(searchResults.posts.length).toEqual(1);
|
||||
expect(searchResults.posts[0].url).toEqual('http://localhost/ghost/visting-china-as-a-polyglot/');
|
||||
|
||||
// out of order English:
|
||||
searchResults = searchIndex.search('glenish');
|
||||
expect(searchResults.posts.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user