aboutsummaryrefslogtreecommitdiff
path: root/dokka-subprojects/plugin-base-frontend/src/main/components/search
diff options
context:
space:
mode:
authorIgnat Beresnev <ignat.beresnev@jetbrains.com>2023-11-10 11:46:54 +0100
committerGitHub <noreply@github.com>2023-11-10 11:46:54 +0100
commit8e5c63d035ef44a269b8c43430f43f5c8eebfb63 (patch)
tree1b915207b2b9f61951ddbf0ff2e687efd053d555 /dokka-subprojects/plugin-base-frontend/src/main/components/search
parenta44efd4ba0c2e4ab921ff75e0f53fc9335aa79db (diff)
downloaddokka-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')
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaFuzzyFilter.tsx101
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/dokkaSearchAnchor.tsx32
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/search.scss118
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/search.tsx74
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/searchResultRow.tsx36
-rw-r--r--dokka-subprojects/plugin-base-frontend/src/main/components/search/types.ts55
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
+}