diff options
| author | Ignat Beresnev <ignat.beresnev@jetbrains.com> | 2023-11-10 11:46:54 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-10 11:46:54 +0100 |
| commit | 8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch) | |
| tree | 1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base-frontend/src/main/components/search | |
| parent | a44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff) | |
| download | dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.gz dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.tar.bz2 dokka-8e5c63d035ef44a269b8c43430f43f5c8eebfb63.zip | |
Restructure the project to utilize included builds (#3174)
* Refactor and simplify artifact publishing
* Update Gradle to 8.4
* Refactor and simplify convention plugins and build scripts
Fixes #3132
---------
Co-authored-by: Adam <897017+aSemy@users.noreply.github.com>
Co-authored-by: Oleg Yukhnevich <whyoleg@gmail.com>
Diffstat (limited to 'dokka-subprojects/plugin-base-frontend/src/main/components/search')
6 files changed, 416 insertions, 0 deletions
diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaFuzzyFilter.tsx b/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaFuzzyFilter.tsx new file mode 100644 index 00000000..0a7edcb3 --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaFuzzyFilter.tsx @@ -0,0 +1,101 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import Select from '@jetbrains/ring-ui/components/select/select'; +import {Option, OptionWithHighlightComponent, OptionWithSearchResult} from "./types"; +import fuzzyHighlight from '@jetbrains/ring-ui/components/global/fuzzy-highlight.js' +import React from "react"; +import {SearchResultRow} from "./searchResultRow"; +import _ from "lodash"; + +const orderRecords = (records: OptionWithSearchResult[], searchPhrase: string): OptionWithSearchResult[] => { + return records.sort((a: OptionWithSearchResult, b: OptionWithSearchResult) => { + //Prefer higher rank + const byRank = a.rank - b.rank + if(byRank !== 0){ + return byRank + } + //Prefer exact matches + const aIncludes = a.name.toLowerCase().includes(searchPhrase.toLowerCase()) ? 1 : 0 + const bIncludes = b.name.toLowerCase().includes(searchPhrase.toLowerCase()) ? 1 : 0 + const byIncludes = bIncludes - aIncludes + if(byIncludes != 0){ + return byIncludes + } + + //Prefer matches that are closer + const byFirstMatchedPosition = a.highlight.indexOf("**") - b.highlight.indexOf("**") + if(byFirstMatchedPosition == 0) { + return a.name.toLowerCase().localeCompare(b.name.toLowerCase()) + } + return byFirstMatchedPosition + }) +} + +const highlightMatchedPhrases = (records: OptionWithSearchResult[]): OptionWithHighlightComponent[] => { + // @ts-ignore + return records.map(record => { + return { + ...record, + template: <SearchResultRow searchResult={record}/> + } + }) +} +export class DokkaFuzzyFilterComponent extends Select { + componentDidUpdate(prevProps, prevState) { + super.componentDidUpdate(prevProps, prevState) + if(this.props.filter && this.state.filterValue != this.props.filter.value){ + this.setState({ + filterValue: this.props.filter.value, + }) + } + } + + _showPopup(){ + if(this.props.shouldShowPopup){ + if (!this.node) { + return; + } + + const shownData = this.getListItems(this.filterValue()); + this.setState({ + showPopup: this.props.shouldShowPopup(this.filterValue()), + shownData + }) + } else { + super._showPopup() + } + } + + getListItems(rawFilterString: string, e: Option[]) { + const filterPhrase = (rawFilterString ? rawFilterString : '').trim() + const matchedRecords = this.props.data + .map((record: Option) => { + const searched = record.searchKeys.map((value, index) => { + return { + ...fuzzyHighlight(filterPhrase, value, false), + ...record, + rank: index + } + }).filter((e) => e.matched) + + const first = _.head(searched) + + if(first){ + return first + } + + return { + matched: false, + ...record, + } + + }) + .filter((record: OptionWithSearchResult) => record.matched) + + this.props.onFilter(filterPhrase) + + return highlightMatchedPhrases(orderRecords(matchedRecords, filterPhrase)) + } +} diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaSearchAnchor.tsx b/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaSearchAnchor.tsx new file mode 100644 index 00000000..f7c6cf46 --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaSearchAnchor.tsx @@ -0,0 +1,32 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import React from "react"; +import Tooltip from '@jetbrains/ring-ui/components/tooltip/tooltip'; +import SearchIcon from 'react-svg-loader!../assets/searchIcon.svg'; +import {CustomAnchorProps} from "./types"; +import {Hotkey} from "../utils/hotkey"; + +const HOTKEY_LETTER = 'k' +const HOTKEY_TOOLTIP_DISPLAY_DELAY = 0.5 * 1000 // seconds + +export const DokkaSearchAnchor = ({wrapperProps, buttonProps, popup}: CustomAnchorProps) => { + const hotkeys = new Hotkey() + hotkeys.registerHotkeyWithAccel(buttonProps.onClick, HOTKEY_LETTER) + + return ( + <span {...wrapperProps}> + <Tooltip + title={`${hotkeys.getOsAccelKeyName()} + ${HOTKEY_LETTER.toUpperCase()}`} + delay={HOTKEY_TOOLTIP_DISPLAY_DELAY} + popupProps={{className: "search-hotkey-popup"}} + > + <button type="button" {...buttonProps}> + <SearchIcon/> + </button> + </Tooltip> + {popup} + </span> + ) +} diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.scss b/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.scss new file mode 100644 index 00000000..6dd07d5b --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.scss @@ -0,0 +1,118 @@ +/*! + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +$font-color: hsla(0, 0%, 100%, 0.8); +$secondary-font-color: hsla(0, 0%, 100%, 0.6); + +#pages-search { + cursor: pointer; + border: none; + border-radius: 50%; + background: transparent; + fill: #fff; + fill: var(--dark-mode-and-search-icon-color); + + &:focus { + outline: none; + } + + &:hover { + background: var(--white-10); + } +} + +.search { + &, [data-test="ring-select"], [data-test="ring-tooltip"], [data-test="ring-select_focus"], #pages-search { + display: inline-block; + padding: 0; + margin: 0; + font-size: 0; + line-height: 0; + } +} + +.search-hotkey-popup { + background-color: var(--background-color) !important; + padding: 4px; +} + +.popup-wrapper { + min-width: calc(100% - 322px) !important; + + border: 1px solid hsla(0, 0%, 100%, 0.2) !important; + + background-color: #27282c !important; + + [class^="filterWrapper"] { + border-bottom: 1px solid hsla(0, 0%, 100%, 0.2); + } + + input { + color: $font-color !important; + + font-weight: normal !important; + } + + span[data-test-custom="ring-select-popup-filter-icon"] { + color: #fff; + } + + button[data-test="ring-input-clear"] { + color: #fff !important; + } +} + +@media screen and (max-width: 759px) { + .popup-wrapper { + min-width: 100% !important; + } +} + +.template-wrapper { + display: grid; + + height: 32px; + grid-template-columns: auto auto; + + strong { + color: $font-color; + } + + span { + color: $font-color; + + line-height: 32px; + + &.template-description { + color: $secondary-font-color; + justify-self: end; + } + } +} + +@media screen and (max-width: 759px) { + .template-wrapper { + display: flex; + flex-direction: column; + + height: auto; + + span { + line-height: unset; + } + } +} + +.template-name { + justify-self: start; +} + +/* remove fade at the bottom */ +[class^="fade"] { + display: none; +} + +[class*="hover"] { + background-color: hsla(0, 0%, 100%, 0.1) !important; +} diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.tsx b/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.tsx new file mode 100644 index 00000000..24545671 --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/search.tsx @@ -0,0 +1,74 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import React, {useCallback, useEffect, useState} from 'react'; +import List from '@jetbrains/ring-ui/components/list/list'; +import Select from '@jetbrains/ring-ui/components/select/select'; +import '@jetbrains/ring-ui/components/input-size/input-size.css'; +import './search.scss'; +import {CustomAnchorProps, IWindow, Option, Props} from "./types"; +import {DokkaSearchAnchor} from "./dokkaSearchAnchor"; +import {DokkaFuzzyFilterComponent} from "./dokkaFuzzyFilter"; +import {relativizeUrlForRequest} from '../utils/requests'; + +const WithFuzzySearchFilterComponent: React.FC<Props> = ({ data }: Props) => { + const [selected, onSelected] = useState<Option>(data[0]); + const onChangeSelected = useCallback( + (option: Option) => { + window.location.replace(`${(window as IWindow).pathToRoot}${option.location}?query=${option.name}`) + onSelected(option); + }, + [data] + ); + + return ( + <div className="search-container"> + <div className="search"> + <DokkaFuzzyFilterComponent + id="pages-search" + selectedLabel="Search" + label="Please type page name" + filter={true} + type={Select.Type.CUSTOM} + clear + renderOptimization + disableScrollToActive + selected={selected} + data={data} + popupClassName={"popup-wrapper"} + onSelect={onChangeSelected} + customAnchor={({ wrapperProps, buttonProps, popup }: CustomAnchorProps) => + <DokkaSearchAnchor wrapperProps={wrapperProps} buttonProps={buttonProps} popup={popup} /> + } + /> + </div> + </div> + ) +} + +export const WithFuzzySearchFilter = () => { + const [navigationList, setNavigationList] = useState<Option[]>([]); + + useEffect(() => { + fetch(relativizeUrlForRequest('scripts/pages.json')) + .then(response => response.json()) + .then((result) => { + setNavigationList(result.map((record: Option, idx: number) => { + return { + ...record, + label: record.name, + key: idx, + type: record.kind, + rgItemType: List.ListProps.Type.CUSTOM + } + })) + }, + (error) => { + console.error('failed to fetch pages data', error) + setNavigationList([]) + }) + }, []) + + return <WithFuzzySearchFilterComponent data={navigationList} />; +}; diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/searchResultRow.tsx b/dokka-subprojects/plugin-base-frontend/src/main/components/search/searchResultRow.tsx new file mode 100644 index 00000000..e8b91519 --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/searchResultRow.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import React from "react"; +import {OptionWithSearchResult, SearchProps} from "./types"; +import _ from "lodash"; + +type HighlighterProps = { + label: string +} + +const Highlighter: React.FC<HighlighterProps> = ({label}: HighlighterProps) => { + return <strong>{label}</strong> +} + +export const signatureFromSearchResult = (searchResult: OptionWithSearchResult): string => { + return searchResult.name.replace(searchResult.searchKeys[searchResult.rank], searchResult.highlight) +} + +export const SearchResultRow: React.FC<SearchProps> = ({searchResult}: SearchProps) => { + /* + This is a work-around for an issue: https://youtrack.jetbrains.com/issue/RG-2108 + */ + const out = _.chunk(signatureFromSearchResult(searchResult).split('**'), 2).flatMap(([txt, label]) => [ + txt, + label ? <Highlighter label={label}></Highlighter> : null, + ]); + + return ( + <div className="template-wrapper"> + <span>{out}</span> + <span className="template-description">{searchResult.description}</span> + </div> + ) +} diff --git a/dokka-subprojects/plugin-base-frontend/src/main/components/search/types.ts b/dokka-subprojects/plugin-base-frontend/src/main/components/search/types.ts new file mode 100644 index 00000000..3e390dde --- /dev/null +++ b/dokka-subprojects/plugin-base-frontend/src/main/components/search/types.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2023 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +import React, {ButtonHTMLAttributes, HTMLAttributes, ReactNode, RefCallback} from "react"; + +export type Page = { + name: string; + kind: string; + location: string; + searchKeys: string[]; + description: string; +} + +export type Option = Page & { + label: string; + key: number; + location: string; + name: string; +} + +export type IWindow = typeof window & { + pathToRoot: string + pages: Page[] +} + +export type Props = { + data: Option[] +}; + +export type OptionWithSearchResult = Option & { + matched: boolean, + highlight: string, + rank: number +} + +export type OptionWithHighlightComponent = Option & { + name: React.FC<SearchProps> +} + +export type SearchProps = { + searchResult: OptionWithSearchResult, +} + +export interface DataTestProps { + 'data-test'?: string | null | undefined +} + +export interface CustomAnchorProps { + wrapperProps: HTMLAttributes<HTMLElement> & DataTestProps & {ref: RefCallback<HTMLElement>} + buttonProps: Pick<ButtonHTMLAttributes<HTMLButtonElement>, 'id' | 'disabled' | 'children'> & + {onClick: () => void} & + DataTestProps, + popup: ReactNode +} |
