diff options
author | Marcin Aman <maman@virtuslab.com> | 2020-07-30 15:05:06 +0200 |
---|---|---|
committer | Paweł Marks <Kordyjan@users.noreply.github.com> | 2020-08-21 15:40:12 +0200 |
commit | 3c07bc1966c85de3351602c4e8798fa507c40e49 (patch) | |
tree | ef39ea18a4ec699e4cab9dc55489b43fccb1e247 /plugins | |
parent | c2b02c1fc17b839075b7cb6fd42498a519473fae (diff) | |
download | dokka-3c07bc1966c85de3351602c4e8798fa507c40e49.tar.gz dokka-3c07bc1966c85de3351602c4e8798fa507c40e49.tar.bz2 dokka-3c07bc1966c85de3351602c4e8798fa507c40e49.zip |
Create navigation search component
Diffstat (limited to 'plugins')
14 files changed, 197 insertions, 31 deletions
diff --git a/plugins/base/build.gradle.kts b/plugins/base/build.gradle.kts index a7807223..9ffecbd7 100644 --- a/plugins/base/build.gradle.kts +++ b/plugins/base/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { api(project(":kotlin-analysis")) implementation("org.jsoup:jsoup:1.12.1") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.1") testImplementation(project(":test-tools")) testImplementation(project(":plugins:base:test-utils")) diff --git a/plugins/base/frontend/src/main/components/navigationPaneSearch/clear.svg b/plugins/base/frontend/src/main/components/navigationPaneSearch/clear.svg new file mode 100644 index 00000000..ad6a2026 --- /dev/null +++ b/plugins/base/frontend/src/main/components/navigationPaneSearch/clear.svg @@ -0,0 +1,3 @@ +<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M11.1374 1.80464L6.94205 5.99996L11.1374 10.1953L10.1947 11.138L5.99935 6.94267L1.80403 11.138L0.861328 10.1953L5.05664 5.99996L0.861328 1.80464L1.80403 0.861938L5.99935 5.05725L10.1947 0.861938L11.1374 1.80464Z" fill="#637282"/> +</svg> diff --git a/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.scss b/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.scss new file mode 100644 index 00000000..b5714ca4 --- /dev/null +++ b/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.scss @@ -0,0 +1,37 @@ +@import "src/main/scss/index.scss"; + +$defaultHeight: 40px; + +div#paneSearch { + + width: 248px; + margin: 0 auto; + + input#navigation-pane-search { + background: $white; + border: 1px solid $grey-border; + box-sizing: border-box; + border-radius: 4px; + padding: 8px; + height: $defaultHeight; + } + + .navigation-pane-search { + width: 100% !important; + padding-top: 16px; + } + + div.paneSearchInputWrapper { + position: relative; + span.paneSearchInputClearIcon { + position: absolute; + top: calc(50% + 2px); //Just to include a border + right: 8px; + cursor: pointer; + } + } +} + +.navigation-pane-popup { + margin-top: 1.2em; +}
\ No newline at end of file diff --git a/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.tsx b/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.tsx new file mode 100644 index 00000000..3174b023 --- /dev/null +++ b/plugins/base/frontend/src/main/components/navigationPaneSearch/navigationPaneSearch.tsx @@ -0,0 +1,73 @@ +import React, { useCallback, useState, useEffect } from 'react'; +import {Select, List } from '@jetbrains/ring-ui'; +import { DokkaFuzzyFilterComponent } from '../search/dokkaFuzzyFilter'; +import { IWindow, Option } from '../search/types'; +import './navigationPaneSearch.scss'; +import ClearIcon from 'react-svg-loader!./clear.svg'; + +export const NavigationPaneSearch = () => { + const defaultWidth = 300 + + const [navigationList, setNavigationList] = useState<Option[]>([]); + const [selected, onSelected] = useState<Option | null>(null); + const [minWidth, setMinWidth] = useState<number>(defaultWidth); + const [filterValue, setFilterValue] = useState<string>('') + + const onChangeSelected = useCallback( + (element: Option) => { + window.location.replace(`${(window as IWindow).pathToRoot}${element.location}`) + onSelected(element); + }, + [selected] + ); + + const onFilter = (filterValue: string, filteredRecords?: Option[]) => { + if(filteredRecords){ + const requiredWidth = Math.max(...filteredRecords.map(e => e.label.length*9), defaultWidth) + setMinWidth(requiredWidth) + } + setFilterValue(filterValue) + } + + const onClearClick = () => { + setFilterValue('') + } + + useEffect(() => { + const pathToRoot = (window as IWindow).pathToRoot + const url = pathToRoot.endsWith('/') ? `${pathToRoot}scripts/navigation-pane.json` : `${pathToRoot}/scripts/navigation-pane.json` + fetch(url) + .then(response => response.json()) + .then((result) => { + setNavigationList(result.map((record: Option) => { + return { + ...record, + rgItemType: List.ListProps.Type.CUSTOM + } + })) + }, + (error) => { + console.error('failed to fetch navigationPane data', error) + setNavigationList([]) + }) + }, []) + + + return <div className={"paneSearchInputWrapper"}> + <DokkaFuzzyFilterComponent + id="navigation-pane-search" + className="navigation-pane-search" + inputPlaceholder="Title filter" + clear={true} + type={Select.Type.INPUT_WITHOUT_CONTROLS} + filter={{fuzzy:true, value: filterValue}} + selected={selected} + data={navigationList} + popupClassName={"navigation-pane-popup"} + onSelect={onChangeSelected} + onFilter={onFilter} + minWidth={minWidth} + /> + <span className={"paneSearchInputClearIcon"} onClick={onClearClick}><ClearIcon /></span> + </div> +}
\ No newline at end of file diff --git a/plugins/base/frontend/src/main/components/root.tsx b/plugins/base/frontend/src/main/components/root.tsx index 70ed9550..9f3ecba9 100644 --- a/plugins/base/frontend/src/main/components/root.tsx +++ b/plugins/base/frontend/src/main/components/root.tsx @@ -4,15 +4,24 @@ import RedBox from 'redbox-react'; import App from "./app"; import './app/index.scss'; +import { NavigationPaneSearch } from './navigationPaneSearch/navigationPaneSearch'; const appEl = document.getElementById('searchBar'); const rootEl = document.createElement('div'); +const renderNavigationPane = () => { + render( + <NavigationPaneSearch />, + document.getElementById('paneSearch') + ) +} + let renderApp = () => { render( <App/>, rootEl ); + renderNavigationPane(); }; // @ts-ignore diff --git a/plugins/base/frontend/src/main/components/search/dokkaFuzzyFilter.tsx b/plugins/base/frontend/src/main/components/search/dokkaFuzzyFilter.tsx index 725fbaee..2d9dcb3d 100644 --- a/plugins/base/frontend/src/main/components/search/dokkaFuzzyFilter.tsx +++ b/plugins/base/frontend/src/main/components/search/dokkaFuzzyFilter.tsx @@ -39,10 +39,20 @@ const highlightMatchedPhrases = (records: OptionWithSearchResult[]): OptionWithH } 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, + }) + } + } + getListItems(rawFilterString: string, _: Option[]) { + const filterPhrase = (rawFilterString ? rawFilterString : '').trim() const matchedRecords = this.props.data .map((record: Option) => { - const bySearchKey = fuzzyHighlight(rawFilterString.trim(), record.searchKey, false) + const bySearchKey = fuzzyHighlight(filterPhrase, record.searchKey, false) if(bySearchKey.matched){ return { ...bySearchKey, @@ -51,13 +61,15 @@ export class DokkaFuzzyFilterComponent extends Select { } } return { - ...fuzzyHighlight(rawFilterString.trim(), record.name, false), + ...fuzzyHighlight(filterPhrase, record.name, false), ...record, rank: SearchRank.NameMatch } }) .filter((record: OptionWithSearchResult) => record.matched) - return highlightMatchedPhrases(orderRecords(matchedRecords, rawFilterString)) + this.props.onFilter(filterPhrase, matchedRecords) + + return highlightMatchedPhrases(orderRecords(matchedRecords, filterPhrase)) } }
\ No newline at end of file diff --git a/plugins/base/frontend/src/main/components/search/search.scss b/plugins/base/frontend/src/main/components/search/search.scss index 1068fe7a..e708ef84 100644 --- a/plugins/base/frontend/src/main/components/search/search.scss +++ b/plugins/base/frontend/src/main/components/search/search.scss @@ -14,20 +14,12 @@ min-width: calc(100% - 360px) !important; } -.indented { - text-indent: 10px; -} - -.disabled { - color: gray; -} - .template-wrapper { + height: 32px; display: grid; grid-template-columns: auto auto; - - span.phraseHighlight { - font-weight: bold; + span { + line-height: 32px; } } diff --git a/plugins/base/frontend/src/main/components/search/search.tsx b/plugins/base/frontend/src/main/components/search/search.tsx index c7976edb..ef26c662 100644 --- a/plugins/base/frontend/src/main/components/search/search.tsx +++ b/plugins/base/frontend/src/main/components/search/search.tsx @@ -20,6 +20,7 @@ const WithFuzzySearchFilterComponent: React.FC<Props> = ({data}: Props) => { <div className="search-container"> <div className="search"> <DokkaFuzzyFilterComponent + id="pages-search" selectedLabel="Search" label="Please type page name" filter={true} diff --git a/plugins/base/frontend/src/main/components/search/types.ts b/plugins/base/frontend/src/main/components/search/types.ts index 922935bd..11e2edf8 100644 --- a/plugins/base/frontend/src/main/components/search/types.ts +++ b/plugins/base/frontend/src/main/components/search/types.ts @@ -6,7 +6,6 @@ export type Page = { location: string; searchKey: string; description: string; - disabled: boolean; } export type Option = Page & { diff --git a/plugins/base/frontend/src/main/scss/index.scss b/plugins/base/frontend/src/main/scss/index.scss index 74af970d..18e2861b 100644 --- a/plugins/base/frontend/src/main/scss/index.scss +++ b/plugins/base/frontend/src/main/scss/index.scss @@ -1 +1,4 @@ @import "~@jetbrains/ring-ui/components/global/variables.css"; + +$white: #FFFFFF; +$grey-border: #A6AFBA diff --git a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt index 83a0fc5b..3d0ced06 100644 --- a/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/DefaultRenderer.kt @@ -7,6 +7,7 @@ import kotlinx.coroutines.runBlocking import org.jetbrains.dokka.DokkaException import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.base.resolvers.local.resolveOrThrow import org.jetbrains.dokka.model.DisplaySourceSet import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext @@ -168,6 +169,9 @@ abstract class DefaultRenderer<T>( is RenderingStrategy.Copy -> outputWriter.writeResources(strategy.from, path) is RenderingStrategy.Write -> outputWriter.write(path, strategy.text, "") is RenderingStrategy.Callback -> outputWriter.write(path, strategy.instructions(this, page), ".html") + is RenderingStrategy.LocationResolvableWrite -> outputWriter.write(path, strategy.contentToResolve { dri, sourcesets -> + locationProvider.resolveOrThrow(dri, sourcesets) + }, "") RenderingStrategy.DoNothing -> Unit } else -> throw AssertionError( diff --git a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt index 5747c6da..22fb3e63 100644 --- a/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt +++ b/plugins/base/src/main/kotlin/renderers/html/HtmlRenderer.kt @@ -675,6 +675,9 @@ open class HtmlRenderer( id = "logo" } div { + id = "paneSearch" + } + div { id = "sideMenu" } } diff --git a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt index a6a16412..f569ed12 100644 --- a/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt +++ b/plugins/base/src/main/kotlin/renderers/html/NavigationPage.kt @@ -38,7 +38,7 @@ class NavigationPage(val root: NavigationNode) : RendererSpecificPage { } } -class NavigationNode( +data class NavigationNode( val name: String, val dri: DRI, val sourceSets: Set<DisplaySourceSet>, diff --git a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt index 407e5899..6e779931 100644 --- a/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt +++ b/plugins/base/src/main/kotlin/renderers/html/htmlPreprocessors.kt @@ -1,20 +1,18 @@ package org.jetbrains.dokka.base.renderers.html +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import kotlinx.html.h1 import kotlinx.html.id import kotlinx.html.table import kotlinx.html.tbody -import org.jetbrains.dokka.DokkaConfiguration -import org.jetbrains.dokka.Platform -import org.jetbrains.dokka.base.DokkaBase import org.jetbrains.dokka.base.renderers.sourceSets -import org.jetbrains.dokka.base.resolvers.local.LocationProvider +import org.jetbrains.dokka.links.DRI import org.jetbrains.dokka.model.DEnum import org.jetbrains.dokka.model.DEnumEntry import org.jetbrains.dokka.pages.* import org.jetbrains.dokka.plugability.DokkaContext -import org.jetbrains.dokka.plugability.plugin -import org.jetbrains.dokka.plugability.querySingle import org.jetbrains.dokka.transformers.pages.PageTransformer @@ -40,19 +38,45 @@ object SearchPageInstaller : PageTransformer { } object NavigationPageInstaller : PageTransformer { - override fun invoke(input: RootPageNode) = input.modified( - children = input.children + NavigationPage( - input.children.filterIsInstance<ContentPage>().single() - .let(NavigationPageInstaller::visit) + private val mapper = jacksonObjectMapper() + + private data class NavigationNodeView( + val name: String, + val label: String = name, + val searchKey: String = name, + @get:JsonSerialize(using = ToStringSerializer::class) val dri: DRI, + val location: String + ) { + companion object { + fun from(node: NavigationNode, location: String): NavigationNodeView = + NavigationNodeView(name = node.name, dri = node.dri, location = location) + } + } + + override fun invoke(input: RootPageNode): RootPageNode { + val nodes = input.children.filterIsInstance<ContentPage>().single() + .let(NavigationPageInstaller::visit) + + val page = RendererSpecificResourcePage( + name = "scripts/navigation-pane.json", + children = emptyList(), + strategy = RenderingStrategy.LocationResolvableWrite { resolver -> + val flattened = flattenNavigationNodes(listOf(nodes)) + val view = flattened.map { NavigationNodeView.from(it, resolver(it.dri, it.sourceSets)) } + mapper.writeValueAsString(view) + }) + + return input.modified( + children = input.children + page + NavigationPage(nodes) ) - ) + } private fun visit(page: ContentPage): NavigationNode = NavigationNode( - page.name, - page.dri.first(), - page.sourceSets(), - page.navigableChildren() + name = page.name, + dri = page.dri.first(), + sourceSets = page.sourceSets(), + children = page.navigableChildren() ) private fun ContentPage.navigableChildren(): List<NavigationNode> = @@ -63,6 +87,11 @@ object NavigationPageInstaller : PageTransformer { children.filter { it is ContentPage && it.documentable is DEnumEntry }.map { visit(it as ContentPage) } else -> emptyList() }.sortedBy { it.name.toLowerCase() } + + private tailrec fun flattenNavigationNodes(nodes: List<NavigationNode>, acc: List<NavigationNode> = emptyList()): List<NavigationNode> { + if(nodes.isEmpty()) return acc + return flattenNavigationNodes(nodes.flatMap { it.children }, nodes.map { it.copy(children = emptyList()) } + acc) + } } object ResourceInstaller : PageTransformer { |