Skip to content
2 changes: 0 additions & 2 deletions js/src/forum/compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ import PostPreview from './components/PostPreview';
import EventPost from './components/EventPost';
import DiscussionHero from './components/DiscussionHero';
import PostMeta from './components/PostMeta';
import SearchSource from './components/SearchSource';
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
import DiscussionComposer from './components/DiscussionComposer';
import LogInButtons from './components/LogInButtons';
Expand Down Expand Up @@ -127,7 +126,6 @@ export default Object.assign(compat, {
'components/EventPost': EventPost,
'components/DiscussionHero': DiscussionHero,
'components/PostMeta': PostMeta,
'components/SearchSource': SearchSource,
'components/DiscussionRenamedPost': DiscussionRenamedPost,
'components/DiscussionComposer': DiscussionComposer,
'components/LogInButtons': LogInButtons,
Expand Down
60 changes: 0 additions & 60 deletions js/src/forum/components/DiscussionsSearchSource.js

This file was deleted.

54 changes: 54 additions & 0 deletions js/src/forum/components/DiscussionsSearchSource.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import highlight from '../../common/helpers/highlight';
import LinkButton from '../../common/components/LinkButton';
import Link from '../../common/components/Link';
import { SearchSource } from './Search';
import Mithril from 'mithril';

/**
* The `DiscussionsSearchSource` finds and displays discussion search results in
* the search dropdown.
*/
export default class DiscussionsSearchSource implements SearchSource {
protected results = new Map<string, unknown[]>();

search(query: string) {
query = query.toLowerCase();

this.results.set(query, []);

const params = {
filter: { q: query },
page: { limit: 3 },
include: 'mostRelevantPost',
};

return app.store.find('discussions', params).then((results) => this.results.set(query, results));
}
Comment on lines +14 to +26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be better off as an async function? It would reduce the callbacks and would automate the promise return.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That wouldn't be any changes other than just the modifier, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would mean using await instead of .then.

E.g.

fetch("example.com").then(response => doSomething(response)

Would become

const response = await fetch("example.com")
doSomething(response)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well yes, but we wouldn't actually want it to be blocking. So we'd still be using then.


view(query: string): Array<Mithril.Vnode> {
Copy link
Member

@davwheat davwheat May 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe Mithril.Children could be a cleaner type as it's used within Mithril itself.

Thinking again, it's probably best left as it is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one needs to be an array

query = query.toLowerCase();

const results = (this.results.get(query) || []).map((discussion: unknown) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of or-ing against an empty array, could we do ?.map?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need it to be a list even if it's undefined.

const mostRelevantPost = discussion.mostRelevantPost();

return (
<li className="DiscussionSearchResult" data-index={'discussions' + discussion.id()}>
<Link href={app.route.discussion(discussion, mostRelevantPost && mostRelevantPost.number())}>
<div className="DiscussionSearchResult-title">{highlight(discussion.title(), query)}</div>
{mostRelevantPost ? <div className="DiscussionSearchResult-excerpt">{highlight(mostRelevantPost.contentPlain(), query, 100)}</div> : ''}
</Link>
</li>
);
}) as Array<Mithril.Vnode>;

return [
<li className="Dropdown-header">{app.translator.trans('core.forum.search.discussions_heading')}</li>,
<li>
<LinkButton icon="fas fa-search" href={app.route('index', { q: query })}>
{app.translator.trans('core.forum.search.all_discussions_button', { query })}
</LinkButton>
</li>,
...results,
];
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import Component from '../../common/Component';
import Component, { ComponentAttrs } from '../../common/Component';
import LoadingIndicator from '../../common/components/LoadingIndicator';
import ItemList from '../../common/utils/ItemList';
import classList from '../../common/utils/classList';
import extractText from '../../common/utils/extractText';
import KeyboardNavigatable from '../utils/KeyboardNavigatable';
import icon from '../../common/helpers/icon';
import SearchState from '../states/SearchState';
import DiscussionsSearchSource from './DiscussionsSearchSource';
import UsersSearchSource from './UsersSearchSource';
import Mithril from 'mithril';

/**
* The `SearchSource` interface defines a section of search results in the
* search dropdown.
*
* Search sources should be registered with the `Search` component class
* by extending the `sourceItems` method. When the user types a
* query, each search source will be prompted to load search results via the
* `search` method. When the dropdown is redrawn, it will be constructed by
* putting together the output from the `view` method of each source.
*/
export interface SearchSource {
/**
* Make a request to get results for the given query.
*/
search(query: string);

/**
* Get an array of virtual <li>s that list the search results for the given
* query.
*/
view(query: string): Array<Mithril.Vnode>;
}

export interface SearchAttrs extends ComponentAttrs {
/** The type of alert this is. Will be used to give the alert a class name of `Alert--{type}`. */
state: SearchState;
}

/**
* The `Search` component displays a menu of as-you-type results from a variety
Expand All @@ -20,53 +50,52 @@ import UsersSearchSource from './UsersSearchSource';
*
* - state: SearchState instance.
*/
export default class Search extends Component {
export default class Search<T extends SearchAttrs = SearchAttrs> extends Component<T> {
static MIN_SEARCH_LEN = 3;

oninit(vnode) {
super.oninit(vnode);
this.state = this.attrs.state;
protected state!: SearchState;

/**
* Whether or not the search input has focus.
*
* @type {Boolean}
*/
this.hasFocus = false;

/**
* An array of SearchSources.
*
* @type {SearchSource[]}
*/
this.sources = null;

/**
* The number of sources that are still loading results.
*
* @type {Integer}
*/
this.loadingSources = 0;
/**
* Whether or not the search input has focus.
*/
protected hasFocus = false;

/**
* The index of the currently-selected <li> in the results list. This can be
* a unique string (to account for the fact that an item's position may jump
* around as new results load), but otherwise it will be numeric (the
* sequential position within the list).
*
* @type {String|Integer}
*/
this.index = 0;
/**
* An array of SearchSources.
*/
protected sources!: SearchSource[];

/**
* The number of sources that are still loading results.
*/
protected loadingSources = 0;

/**
* The index of the currently-selected <li> in the results list. This can be
* a unique string (to account for the fact that an item's position may jump
* around as new results load), but otherwise it will be numeric (the
* sequential position within the list).
*/
protected index: number = 0;

protected navigator!: KeyboardNavigatable;

protected searchTimeout?: number;

private updateMaxHeightHandler?: () => void;

oninit(vnode: Mithril.Vnode<T, this>) {
super.oninit(vnode);

this.state = this.attrs.state;
}

view() {
const currentSearch = this.state.getInitialSearch();

// Initialize search sources in the view rather than the constructor so
// that we have access to app.forum.
if (!this.sources) {
this.sources = this.sourceItems().toArray();
}
if (!this.sources) this.sources = this.sourceItems().toArray();

// Hide the search view if no sources were loaded
if (!this.sources.length) return <div></div>;
Expand All @@ -76,15 +105,13 @@ export default class Search extends Component {
return (
<div
role="search"
className={
'Search ' +
classList({
open: this.state.getValue() && this.hasFocus,
focused: this.hasFocus,
active: !!currentSearch,
loading: !!this.loadingSources,
})
}
className={classList({
Search: true,
open: this.state.getValue() && this.hasFocus,
focused: this.hasFocus,
active: !!currentSearch,
loading: !!this.loadingSources,
})}
>
<div className="Search-input">
<input
Expand Down Expand Up @@ -153,7 +180,7 @@ export default class Search extends Component {
search.setIndex(search.selectableItems().index(this));
});

const $input = this.$('input');
const $input = this.$('input') as JQuery<HTMLInputElement>;

this.navigator = new KeyboardNavigatable();
this.navigator
Expand Down Expand Up @@ -233,10 +260,8 @@ export default class Search extends Component {

/**
* Build an item list of SearchSources.
*
* @return {ItemList}
*/
sourceItems() {
sourceItems(): ItemList {
const items = new ItemList();

if (app.forum.attribute('canViewDiscussions')) items.add('discussions', new DiscussionsSearchSource());
Expand All @@ -247,29 +272,22 @@ export default class Search extends Component {

/**
* Get all of the search result items that are selectable.
*
* @return {jQuery}
*/
selectableItems() {
selectableItems(): JQuery {
return this.$('.Search-results > li:not(.Dropdown-header)');
}

/**
* Get the position of the currently selected search result item.
*
* @return {Integer}
*/
getCurrentNumericIndex() {
getCurrentNumericIndex(): number {
return this.selectableItems().index(this.getItem(this.index));
}

/**
* Get the <li> in the search results with the given index (numeric or named).
*
* @param {String} index
* @return {DOMElement}
*/
getItem(index) {
getItem(index: number): JQuery {
const $items = this.selectableItems();
let $item = $items.filter(`[data-index="${index}"]`);

Expand All @@ -283,12 +301,8 @@ export default class Search extends Component {
/**
* Set the currently-selected search result item to the one with the given
* index.
*
* @param {Integer} index
* @param {Boolean} scrollToItem Whether or not to scroll the dropdown so that
* the item is in view.
*/
setIndex(index, scrollToItem) {
setIndex(index: number, scrollToItem: boolean = false) {
const $items = this.selectableItems();
const $dropdown = $items.parent();

Expand All @@ -301,7 +315,7 @@ export default class Search extends Component {

const $item = $items.removeClass('active').eq(fixedIndex).addClass('active');

this.index = $item.attr('data-index') || fixedIndex;
this.index = parseInt($item.attr('data-index') as string) || fixedIndex;

if (scrollToItem) {
const dropdownScroll = $dropdown.scrollTop();
Expand Down
30 changes: 0 additions & 30 deletions js/src/forum/components/SearchSource.js

This file was deleted.

Loading