@open-pioneer/search 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ # @open-pioneer/search
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6209d6c: Initial release
8
+
9
+ ### Patch Changes
10
+
11
+ - 565bd8b: Fix usage via touch (NOTE: currently requires a patch to react-select; see repository's `package.json` file).
12
+ - Updated dependencies [08bffbc]
13
+ - Updated dependencies [a58546b]
14
+ - Updated dependencies [a58546b]
15
+ - Updated dependencies [0c4ce04]
16
+ - @open-pioneer/map@0.1.1
@@ -0,0 +1,11 @@
1
+ import { ClearIndicatorProps, IndicatorsContainerProps, InputProps, MenuProps, NoticeProps, OptionProps, SingleValueProps, ValueContainerProps } from "chakra-react-select";
2
+ import { SearchGroupOption, SearchOption } from "./Search";
3
+ export declare function MenuComp(props: MenuProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
4
+ export declare function NoOptionsMessage(props: NoticeProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
5
+ export declare function LoadingMessage(props: NoticeProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
6
+ export declare function ValueContainer({ children, ...props }: ValueContainerProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
7
+ export declare function Input(props: InputProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
8
+ export declare function SingleValue(_props: SingleValueProps<SearchOption, false, SearchGroupOption>): null;
9
+ export declare function IndicatorsContainer(props: IndicatorsContainerProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
10
+ export declare function ClearIndicator(_props: ClearIndicatorProps<SearchOption, false, SearchGroupOption>): null;
11
+ export declare function HighlightOption(props: OptionProps<SearchOption, false, SearchGroupOption>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,111 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { SearchIcon } from '@chakra-ui/icons';
3
+ import { chakra, CloseButton } from '@open-pioneer/chakra-integration';
4
+ import { chakraComponents } from 'chakra-react-select';
5
+ import classNames from 'classnames';
6
+ import { useIntl } from './_virtual/_virtual-pioneer-module_react-hooks.js';
7
+
8
+ function MenuComp(props) {
9
+ const hasInput = props.selectProps.inputValue.length > 0;
10
+ const menuProps = {
11
+ ...props,
12
+ className: classNames(props.className, {
13
+ "search-invisible": !hasInput
14
+ })
15
+ };
16
+ return /* @__PURE__ */ jsx(chakraComponents.Menu, { ...menuProps, children: props.children });
17
+ }
18
+ function NoOptionsMessage(props) {
19
+ const intl = useIntl();
20
+ const noMessageText = intl.formatMessage({ id: "noOptionsText" });
21
+ return /* @__PURE__ */ jsx(chakraComponents.NoOptionsMessage, { ...props, children: /* @__PURE__ */ jsx(chakra.span, { className: "search-no-match", children: noMessageText }) });
22
+ }
23
+ function LoadingMessage(props) {
24
+ const intl = useIntl();
25
+ const loadingText = intl.formatMessage({ id: "loadingText" });
26
+ return /* @__PURE__ */ jsx(chakraComponents.LoadingMessage, { ...props, children: /* @__PURE__ */ jsx(chakra.span, { className: "search-loading-text", children: loadingText }) });
27
+ }
28
+ function ValueContainer({
29
+ children,
30
+ ...props
31
+ }) {
32
+ const containerProps = {
33
+ ...props,
34
+ className: classNames(props.className, "search-value-container")
35
+ };
36
+ return /* @__PURE__ */ jsxs(chakraComponents.ValueContainer, { ...containerProps, children: [
37
+ !!children && /* @__PURE__ */ jsx(SearchIcon, { style: { position: "absolute", left: 8 } }),
38
+ children
39
+ ] });
40
+ }
41
+ function Input(props) {
42
+ const inputProps = {
43
+ ...props,
44
+ isHidden: false
45
+ };
46
+ return /* @__PURE__ */ jsx(chakraComponents.Input, { ...inputProps });
47
+ }
48
+ function SingleValue(_props) {
49
+ return null;
50
+ }
51
+ function IndicatorsContainer(props) {
52
+ return /* @__PURE__ */ jsxs(chakraComponents.IndicatorsContainer, { ...props, children: [
53
+ props.children,
54
+ !props.selectProps.isLoading && props.selectProps.inputValue && /* @__PURE__ */ jsx(
55
+ CustomClearIndicator,
56
+ {
57
+ selectProps: props.selectProps,
58
+ clearValue: props.clearValue
59
+ }
60
+ )
61
+ ] });
62
+ }
63
+ function CustomClearIndicator(props) {
64
+ const intl = useIntl();
65
+ const clearButtonLabel = intl.formatMessage({
66
+ id: "ariaLabel.clearButton"
67
+ });
68
+ const clickHandler = (e) => {
69
+ e.preventDefault();
70
+ e.stopPropagation();
71
+ props.clearValue();
72
+ };
73
+ return /* @__PURE__ */ jsx(
74
+ CloseButton,
75
+ {
76
+ role: "button",
77
+ size: "md",
78
+ mr: 1,
79
+ "aria-label": clearButtonLabel,
80
+ onClick: clickHandler,
81
+ onTouchEnd: clickHandler,
82
+ onMouseDown: (e) => e.preventDefault()
83
+ }
84
+ );
85
+ }
86
+ function ClearIndicator(_props) {
87
+ return null;
88
+ }
89
+ function HighlightOption(props) {
90
+ const userInput = props.selectProps.inputValue;
91
+ const label = props.data.label;
92
+ const optionProps = {
93
+ ...props,
94
+ className: classNames(props.className, "search-option")
95
+ };
96
+ return /* @__PURE__ */ jsx(chakraComponents.Option, { ...optionProps, children: /* @__PURE__ */ jsx(chakra.div, { className: "search-option-label", children: userInput.trim().length > 0 ? getHighlightedLabel(label, userInput) : label }) });
97
+ }
98
+ function getHighlightedLabel(label, userInput) {
99
+ const matchIndex = label.toLowerCase().indexOf(userInput.toLowerCase());
100
+ if (matchIndex >= 0) {
101
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ label.substring(0, matchIndex),
103
+ /* @__PURE__ */ jsx(chakra.span, { className: "search-highlighted-match", children: label.substring(matchIndex, matchIndex + userInput.length) }, "highlighted"),
104
+ label.substring(matchIndex + userInput.length)
105
+ ] });
106
+ }
107
+ return label;
108
+ }
109
+
110
+ export { ClearIndicator, HighlightOption, IndicatorsContainer, Input, LoadingMessage, MenuComp, NoOptionsMessage, SingleValue, ValueContainer };
111
+ //# sourceMappingURL=CustomComponents.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CustomComponents.js","sources":["CustomComponents.tsx"],"sourcesContent":["// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)\n// SPDX-License-Identifier: Apache-2.0\nimport { SearchIcon } from \"@chakra-ui/icons\";\nimport { CloseButton, chakra } from \"@open-pioneer/chakra-integration\";\nimport {\n ClearIndicatorProps,\n IndicatorsContainerProps,\n InputProps,\n MenuProps,\n NoticeProps,\n OptionProps,\n Props as SelectProps,\n SingleValueProps,\n ValueContainerProps,\n chakraComponents\n} from \"chakra-react-select\";\nimport classNames from \"classnames\";\nimport { useIntl } from \"open-pioneer:react-hooks\";\nimport { UIEvent } from \"react\";\nimport { SearchGroupOption, SearchOption } from \"./Search\";\n\nexport function MenuComp(props: MenuProps<SearchOption, false, SearchGroupOption>) {\n const hasInput = props.selectProps.inputValue.length > 0;\n const menuProps: typeof props = {\n ...props,\n className: classNames(props.className, {\n \"search-invisible\": !hasInput\n })\n };\n\n return <chakraComponents.Menu {...menuProps}>{props.children}</chakraComponents.Menu>;\n}\n\nexport function NoOptionsMessage(props: NoticeProps<SearchOption, false, SearchGroupOption>) {\n const intl = useIntl();\n const noMessageText = intl.formatMessage({ id: \"noOptionsText\" });\n\n return (\n <chakraComponents.NoOptionsMessage {...props}>\n <chakra.span className=\"search-no-match\">{noMessageText}</chakra.span>\n </chakraComponents.NoOptionsMessage>\n );\n}\n\nexport function LoadingMessage(props: NoticeProps<SearchOption, false, SearchGroupOption>) {\n const intl = useIntl();\n const loadingText = intl.formatMessage({ id: \"loadingText\" });\n\n return (\n <chakraComponents.LoadingMessage {...props}>\n <chakra.span className=\"search-loading-text\">{loadingText}</chakra.span>\n </chakraComponents.LoadingMessage>\n );\n}\n\nexport function ValueContainer({\n children,\n ...props\n}: ValueContainerProps<SearchOption, false, SearchGroupOption>) {\n const containerProps: typeof props = {\n ...props,\n className: classNames(props.className, \"search-value-container\")\n };\n return (\n <chakraComponents.ValueContainer {...containerProps}>\n {!!children && <SearchIcon style={{ position: \"absolute\", left: 8 }}></SearchIcon>}\n {children}\n </chakraComponents.ValueContainer>\n );\n}\n\nexport function Input(props: InputProps<SearchOption, false, SearchGroupOption>) {\n const inputProps: typeof props = {\n ...props,\n isHidden: false\n };\n return <chakraComponents.Input {...inputProps} />;\n}\n\nexport function SingleValue(_props: SingleValueProps<SearchOption, false, SearchGroupOption>) {\n // Never render anything (we use the text input to show the selected result)\n return null;\n}\n\nexport function IndicatorsContainer(\n props: IndicatorsContainerProps<SearchOption, false, SearchGroupOption>\n) {\n return (\n <chakraComponents.IndicatorsContainer {...props}>\n {props.children}\n {!props.selectProps.isLoading && props.selectProps.inputValue && (\n <CustomClearIndicator\n selectProps={props.selectProps}\n clearValue={props.clearValue}\n />\n )}\n </chakraComponents.IndicatorsContainer>\n );\n}\n\nfunction CustomClearIndicator(props: {\n clearValue(): void;\n selectProps: SelectProps<SearchOption, false, SearchGroupOption>;\n}) {\n const intl = useIntl();\n const clearButtonLabel = intl.formatMessage({\n id: \"ariaLabel.clearButton\"\n });\n const clickHandler = (e: UIEvent) => {\n e.preventDefault();\n e.stopPropagation();\n props.clearValue();\n };\n\n return (\n <CloseButton\n role=\"button\"\n size=\"md\"\n mr={1}\n aria-label={clearButtonLabel}\n onClick={clickHandler}\n // needed for correct touch handling; select control would otherwise preventDefault()\n onTouchEnd={clickHandler}\n // Stop select component from opening the menu.\n // It will otherwise flash briefly because of a mouse down listener in the select.\n onMouseDown={(e) => e.preventDefault()}\n />\n );\n}\n\nexport function ClearIndicator(\n _props: ClearIndicatorProps<SearchOption, false, SearchGroupOption>\n) {\n // Never render anything; we use our own clear indicator\n return null;\n}\n\nexport function HighlightOption(props: OptionProps<SearchOption, false, SearchGroupOption>) {\n const userInput = props.selectProps.inputValue;\n const label = props.data.label;\n const optionProps: typeof props = {\n ...props,\n className: classNames(props.className, \"search-option\")\n };\n return (\n <chakraComponents.Option {...optionProps}>\n <chakra.div className=\"search-option-label\">\n {userInput.trim().length > 0 ? getHighlightedLabel(label, userInput) : label}\n </chakra.div>\n </chakraComponents.Option>\n );\n}\n\nfunction getHighlightedLabel(label: string, userInput: string) {\n const matchIndex = label.toLowerCase().indexOf(userInput.toLowerCase());\n if (matchIndex >= 0) {\n return (\n <>\n {label.substring(0, matchIndex)}\n <chakra.span key=\"highlighted\" className=\"search-highlighted-match\">\n {label.substring(matchIndex, matchIndex + userInput.length)}\n </chakra.span>\n {label.substring(matchIndex + userInput.length)}\n </>\n );\n }\n return label;\n}\n"],"names":[],"mappings":"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"}
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,109 @@
1
+ # @open-pioneer/search
2
+
3
+ This package provides a UI component to perform a search on given search sources.
4
+
5
+ The search results are presented as a grouped list, one group for each search source.
6
+ This list is sorted by the given order of search sources and the order of the search results inside is
7
+ defined by the implementation of the search source.
8
+
9
+ ## Usage
10
+
11
+ To use the search in your app, insert the following snippet and reference a map ID and `sources` (sources to be searched on):
12
+
13
+ ```tsx
14
+ <Search mapId={MAP_ID} sources={searchsources} />
15
+ ```
16
+
17
+ To change the typing delay, add the optional property `searchTypingDelay` (in ms).
18
+ The default value is 200ms.
19
+
20
+ ```tsx
21
+ <Search mapId={MAP_ID} sources={searchsources} searchTypingDelay={500} />
22
+ ```
23
+
24
+ To limit the maximum number of search results to be displayed per search source (group), configure the optional property `maxResultsPerGroup`.
25
+ The default value is 5.
26
+
27
+ ```tsx
28
+ <Search mapId={MAP_ID} sources={searchsources} maxResultsPerGroup={10} />
29
+ ```
30
+
31
+ ### Listening to events
32
+
33
+ To listen to the events `onSelect` and `onClear`, provide optional callback functions to the component.
34
+ In case of the `onSelect` event, you can access the selected search result (and its search source)
35
+ from the parameter `SelectSearchEvent`.
36
+
37
+ ```tsx
38
+ import { Search, SearchSelectEvent } from "@open-pioneer/search";
39
+ // ...
40
+ <Search
41
+ mapId={MAP_ID}
42
+ sources={datasources}
43
+ onSelect={(event: SearchSelectEvent) => {
44
+ // do something
45
+ }}
46
+ onClear={() => {
47
+ // do something
48
+ }}
49
+ />;
50
+ ```
51
+
52
+ ### Positioning the search bar
53
+
54
+ The search bar is not placed at a certain location by default; it will simply fill its parent.
55
+
56
+ To achieve a certain position in a parent node, the following snippet might be helpful:
57
+
58
+ ```css
59
+ /* top center placement */
60
+ .search-top-center-placement {
61
+ position: absolute;
62
+ left: 50%;
63
+ top: 0px;
64
+ transform: translate(-50%, 0px);
65
+ width: 500px;
66
+ z-index: 1;
67
+ }
68
+ ```
69
+
70
+ ```jsx
71
+ <Box
72
+ // ...
73
+ className="search-top-center-placement"
74
+ >
75
+ <Search /* ... */ />
76
+ </Box>
77
+ ```
78
+
79
+ ### Implementing a search source
80
+
81
+ To provide search sources that are used by the search component, implement the function `search` for each datasource:
82
+
83
+ ```tsx
84
+ import { Search, SearchSource, SearchResult } from "@open-pioneer/search";
85
+ import { MAP_ID } from "./MapConfigProviderImpl";
86
+
87
+ class MySearchSource implements SearchSource {
88
+ // The label of this source, used as a title for this source's results.
89
+ label = "My sample REST-Service";
90
+
91
+ // Attempts to retrieve results for the given input string. For more details,
92
+ // see the API documentation of `SearchSource`.
93
+ async search(inputValue: string, options: SearchOptions): Promise<SearchResult[]> {
94
+ // implement
95
+ }
96
+ }
97
+
98
+ const searchsources: SearchSource[] = [new MySearchSource()];
99
+
100
+ // In your JSX template:
101
+ <Search mapId={MAP_ID} sources={searchsources} />;
102
+ ```
103
+
104
+ The configured maximum number of `maxResultsPerGroup` is passed as `maxResults` inside the option parameter
105
+ of the search function, so you are able to fetch no more results than necessary.
106
+
107
+ ## License
108
+
109
+ Apache-2.0 (see `LICENSE` file)
package/Search.d.ts ADDED
@@ -0,0 +1,63 @@
1
+ import { CommonComponentProps } from "@open-pioneer/react-utils";
2
+ import { FC } from "react";
3
+ import { SearchSource, SearchResult } from "./api";
4
+ export interface SearchOption {
5
+ /** Unique value for this option. */
6
+ value: string;
7
+ /** Display text shown in menu. */
8
+ label: string;
9
+ /** Search source that returned the suggestion. */
10
+ source: SearchSource;
11
+ /** The raw result from the search source. */
12
+ result: SearchResult;
13
+ }
14
+ export interface SearchGroupOption {
15
+ /** Display text shown in menu. */
16
+ label: string;
17
+ /** Set of options that belong to this group. */
18
+ options: SearchOption[];
19
+ }
20
+ /**
21
+ * Event type emitted when the user selects an item.
22
+ */
23
+ export interface SearchSelectEvent {
24
+ /** The source that returned the {@link result}. */
25
+ source: SearchSource;
26
+ /** The search result selected by the user. */
27
+ result: SearchResult;
28
+ }
29
+ /**
30
+ * Properties supported by the {@link Search} component.
31
+ */
32
+ export interface SearchProps extends CommonComponentProps {
33
+ /**
34
+ * The id of the map.
35
+ */
36
+ mapId: string;
37
+ /**
38
+ * Data sources to be searched on.
39
+ */
40
+ sources: SearchSource[];
41
+ /**
42
+ * Typing delay (in milliseconds) before the async search query starts after the user types in the search term.
43
+ * Defaults to `200`.
44
+ */
45
+ searchTypingDelay?: number;
46
+ /**
47
+ * The maximum number of results shown per group.
48
+ * Defaults to `5`.
49
+ */
50
+ maxResultsPerGroup?: number;
51
+ /**
52
+ * This event handler will be called when the user selects a search result.
53
+ */
54
+ onSelect?: (event: SearchSelectEvent) => void;
55
+ /**
56
+ * This event handler will be called when the user clears the search input.
57
+ */
58
+ onClear?: () => void;
59
+ }
60
+ /**
61
+ * A component that allows the user to search a given set of {@link SearchSource | SearchSources}.
62
+ */
63
+ export declare const Search: FC<SearchProps>;
package/Search.js ADDED
@@ -0,0 +1,285 @@
1
+ import { jsx } from 'react/jsx-runtime';
2
+ import { Box, useToken } from '@open-pioneer/chakra-integration';
3
+ import { createLogger, isAbortError } from '@open-pioneer/core';
4
+ import { useMapModel } from '@open-pioneer/map';
5
+ import { useCommonComponentProps, useEvent } from '@open-pioneer/react-utils';
6
+ import { Select } from 'chakra-react-select';
7
+ import { useIntl } from './_virtual/_virtual-pioneer-module_react-hooks.js';
8
+ import { useRef, useMemo, useState, useEffect, useReducer, useCallback } from 'react';
9
+ import { MenuComp, Input, SingleValue, HighlightOption, NoOptionsMessage, LoadingMessage, ValueContainer, IndicatorsContainer, ClearIndicator } from './CustomComponents.js';
10
+ import { SearchController } from './SearchController.js';
11
+
12
+ const LOG = createLogger("search:Search");
13
+ const Search = (props) => {
14
+ const { mapId, sources, searchTypingDelay, maxResultsPerGroup, onSelect, onClear } = props;
15
+ const { containerProps } = useCommonComponentProps("search", props);
16
+ const { map } = useMapModel(mapId);
17
+ const intl = useIntl();
18
+ const controller = useController(sources, searchTypingDelay, maxResultsPerGroup, map);
19
+ const { input, search: search2, selectedOption, onInputChanged, onResultConfirmed } = useSearchState(controller);
20
+ const chakraStyles = useChakraStyles();
21
+ const ariaMessages = useAriaMessages(intl);
22
+ const components = useCustomComponents();
23
+ const handleInputChange = useEvent((newValue, actionMeta) => {
24
+ if (actionMeta.action === "input-change") {
25
+ onInputChanged(newValue);
26
+ }
27
+ });
28
+ const handleSelectChange = useEvent(
29
+ (value, actionMeta) => {
30
+ switch (actionMeta.action) {
31
+ case "select-option":
32
+ if (value) {
33
+ onResultConfirmed(value);
34
+ onSelect?.({
35
+ source: value.source,
36
+ result: value.result
37
+ });
38
+ }
39
+ break;
40
+ case "clear":
41
+ onInputChanged("");
42
+ selectRef.current?.blur();
43
+ selectRef.current?.focus();
44
+ onClear?.();
45
+ break;
46
+ default:
47
+ LOG.debug(`Unhandled action type '${actionMeta.action}'.`);
48
+ break;
49
+ }
50
+ }
51
+ );
52
+ const selectRef = useRef(null);
53
+ return /* @__PURE__ */ jsx(Box, { ...containerProps, children: /* @__PURE__ */ jsx(
54
+ Select,
55
+ {
56
+ className: "search-component",
57
+ classNamePrefix: "react-select",
58
+ ref: selectRef,
59
+ inputValue: input,
60
+ onInputChange: handleInputChange,
61
+ "aria-label": intl.formatMessage({ id: "ariaLabel.search" }),
62
+ ariaLiveMessages: ariaMessages,
63
+ colorScheme: "trails",
64
+ selectedOptionStyle: "color",
65
+ selectedOptionColorScheme: "trails",
66
+ chakraStyles,
67
+ isClearable: true,
68
+ placeholder: intl.formatMessage({ id: "searchPlaceholder" }),
69
+ closeMenuOnSelect: true,
70
+ isLoading: search2.kind === "loading",
71
+ options: search2.kind === "ready" ? search2.results : void 0,
72
+ filterOption: () => true,
73
+ tabSelectsValue: false,
74
+ components,
75
+ onChange: handleSelectChange,
76
+ value: selectedOption
77
+ }
78
+ ) });
79
+ };
80
+ function useAriaMessages(intl) {
81
+ return useMemo(() => {
82
+ const onFocus = ({ focused }) => {
83
+ return `${focused.label} ${intl.formatMessage({ id: "ariaLabel.searchFocus" })}.`;
84
+ };
85
+ const onChange = ({ action, label }) => {
86
+ let message = "";
87
+ switch (action) {
88
+ case "select-option":
89
+ message = `${label} ${intl.formatMessage({ id: "ariaLabel.searchSelect" })}.`;
90
+ break;
91
+ case "clear":
92
+ message = `${label} ${intl.formatMessage({ id: "ariaLabel.searchClear" })}.`;
93
+ break;
94
+ }
95
+ return message;
96
+ };
97
+ const guidance = () => {
98
+ return `${intl.formatMessage({ id: "ariaLabel.instructions" })}`;
99
+ };
100
+ const onFilter = () => {
101
+ return "";
102
+ };
103
+ return {
104
+ onFocus,
105
+ onChange,
106
+ guidance,
107
+ onFilter
108
+ };
109
+ }, [intl]);
110
+ }
111
+ function useCustomComponents() {
112
+ return useMemo(() => {
113
+ return {
114
+ Menu: MenuComp,
115
+ Input,
116
+ SingleValue: SingleValue,
117
+ Option: HighlightOption,
118
+ NoOptionsMessage,
119
+ LoadingMessage,
120
+ ValueContainer,
121
+ IndicatorsContainer,
122
+ ClearIndicator
123
+ };
124
+ }, []);
125
+ }
126
+ function useChakraStyles() {
127
+ const [groupHeadingBg, focussedItemBg] = useToken(
128
+ "colors",
129
+ ["trails.100", "trails.50"],
130
+ ["#d5e5ec", "#eaf2f5"]
131
+ );
132
+ return useMemo(() => {
133
+ const chakraStyles = {
134
+ groupHeading: (provided) => ({
135
+ ...provided,
136
+ backgroundColor: groupHeadingBg,
137
+ padding: "8px 12px",
138
+ // make Header look like normal options:
139
+ fontSize: "inherit",
140
+ fontWeight: "inherit"
141
+ }),
142
+ option: (provided) => ({
143
+ ...provided,
144
+ backgroundColor: "inherit",
145
+ _focus: {
146
+ backgroundColor: focussedItemBg
147
+ }
148
+ }),
149
+ dropdownIndicator: (provided) => ({
150
+ ...provided,
151
+ display: "none"
152
+ // always hide
153
+ })
154
+ };
155
+ return chakraStyles;
156
+ }, [groupHeadingBg, focussedItemBg]);
157
+ }
158
+ function useController(sources, searchTypingDelay, maxResultsPerGroup, map) {
159
+ const [controller, setController] = useState(void 0);
160
+ useEffect(() => {
161
+ if (!map) {
162
+ return;
163
+ }
164
+ const controller2 = new SearchController(map, sources);
165
+ setController(controller2);
166
+ return () => {
167
+ controller2.destroy();
168
+ setController(void 0);
169
+ };
170
+ }, [map, sources]);
171
+ useEffect(() => {
172
+ controller && (controller.searchTypingDelay = searchTypingDelay);
173
+ }, [controller, searchTypingDelay]);
174
+ useEffect(() => {
175
+ controller && (controller.maxResultsPerSource = maxResultsPerGroup);
176
+ }, [controller, maxResultsPerGroup]);
177
+ return controller;
178
+ }
179
+ function useSearchState(controller) {
180
+ const [state, dispatch] = useReducer(
181
+ (current, action) => {
182
+ switch (action.kind) {
183
+ case "input":
184
+ return {
185
+ ...current,
186
+ query: action.query,
187
+ selectedOption: null
188
+ };
189
+ case "select-option":
190
+ return {
191
+ ...current,
192
+ selectedOption: action.option,
193
+ query: action.option.label
194
+ };
195
+ case "load-results":
196
+ return {
197
+ ...current,
198
+ search: {
199
+ kind: "loading"
200
+ }
201
+ };
202
+ case "accept-results":
203
+ return {
204
+ ...current,
205
+ search: {
206
+ kind: "ready",
207
+ results: action.results
208
+ }
209
+ };
210
+ }
211
+ },
212
+ void 0,
213
+ () => ({
214
+ query: "",
215
+ selectedOption: null,
216
+ search: {
217
+ kind: "ready",
218
+ results: []
219
+ }
220
+ })
221
+ );
222
+ const currentSearch = useRef();
223
+ const startSearch = useEvent((query) => {
224
+ if (!controller) {
225
+ currentSearch.current = void 0;
226
+ dispatch({ kind: "accept-results", results: [] });
227
+ return;
228
+ }
229
+ LOG.isDebug() && LOG.debug(`Starting new search for query ${JSON.stringify(query)}.`);
230
+ dispatch({ kind: "load-results" });
231
+ const promise = currentSearch.current = search(controller, query).then((results) => {
232
+ if (currentSearch.current === promise) {
233
+ dispatch({ kind: "accept-results", results });
234
+ }
235
+ });
236
+ });
237
+ const onResultConfirmed = useCallback((option) => {
238
+ dispatch({ kind: "select-option", option });
239
+ }, []);
240
+ const onInputChanged = useCallback(
241
+ (newValue) => {
242
+ dispatch({ kind: "input", query: newValue });
243
+ startSearch(newValue);
244
+ },
245
+ [startSearch]
246
+ );
247
+ return {
248
+ input: state.query,
249
+ search: state.search,
250
+ selectedOption: state.selectedOption,
251
+ onResultConfirmed,
252
+ onInputChanged
253
+ };
254
+ }
255
+ async function search(controller, query) {
256
+ let suggestions;
257
+ try {
258
+ suggestions = await controller.search(query);
259
+ } catch (error) {
260
+ if (!isAbortError(error)) {
261
+ LOG.error(`Search failed`, error);
262
+ }
263
+ suggestions = [];
264
+ }
265
+ return mapSuggestions(suggestions);
266
+ }
267
+ function mapSuggestions(suggestions) {
268
+ const options = suggestions.map(
269
+ (group, groupIndex) => ({
270
+ label: group.label,
271
+ options: group.results.map((suggestion) => {
272
+ return {
273
+ value: `${groupIndex}-${suggestion.id}`,
274
+ label: suggestion.label,
275
+ source: group.source,
276
+ result: suggestion
277
+ };
278
+ })
279
+ })
280
+ );
281
+ return options;
282
+ }
283
+
284
+ export { Search };
285
+ //# sourceMappingURL=Search.js.map
package/Search.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Search.js","sources":["Search.tsx"],"sourcesContent":["// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)\n// SPDX-License-Identifier: Apache-2.0\nimport { Box, useToken } from \"@open-pioneer/chakra-integration\";\nimport { createLogger, isAbortError } from \"@open-pioneer/core\";\nimport { MapModel, useMapModel } from \"@open-pioneer/map\";\nimport { CommonComponentProps, useCommonComponentProps, useEvent } from \"@open-pioneer/react-utils\";\nimport {\n ActionMeta,\n AriaLiveMessages,\n AriaOnChange,\n AriaOnFocus,\n ChakraStylesConfig,\n InputActionMeta,\n Select,\n SelectInstance,\n SingleValue,\n Props as SelectProps\n} from \"chakra-react-select\";\nimport { useIntl } from \"open-pioneer:react-hooks\";\nimport { FC, useCallback, useEffect, useMemo, useReducer, useRef, useState } from \"react\";\nimport {\n ClearIndicator,\n HighlightOption,\n IndicatorsContainer,\n Input,\n LoadingMessage,\n MenuComp,\n NoOptionsMessage,\n SingleValue as SingleValueComp,\n ValueContainer\n} from \"./CustomComponents\";\nimport { SearchController, SuggestionGroup } from \"./SearchController\";\nimport { SearchSource, SearchResult } from \"./api\";\nimport { PackageIntl } from \"@open-pioneer/runtime\";\n\nconst LOG = createLogger(\"search:Search\");\n\nexport interface SearchOption {\n /** Unique value for this option. */\n value: string;\n\n /** Display text shown in menu. */\n label: string;\n\n /** Search source that returned the suggestion. */\n source: SearchSource;\n\n /** The raw result from the search source. */\n result: SearchResult;\n}\n\nexport interface SearchGroupOption {\n /** Display text shown in menu. */\n label: string;\n\n /** Set of options that belong to this group. */\n options: SearchOption[];\n}\n\n/**\n * Event type emitted when the user selects an item.\n */\nexport interface SearchSelectEvent {\n /** The source that returned the {@link result}. */\n source: SearchSource;\n\n /** The search result selected by the user. */\n result: SearchResult;\n}\n\n/**\n * Properties supported by the {@link Search} component.\n */\nexport interface SearchProps extends CommonComponentProps {\n /**\n * The id of the map.\n */\n mapId: string;\n\n /**\n * Data sources to be searched on.\n */\n sources: SearchSource[];\n\n /**\n * Typing delay (in milliseconds) before the async search query starts after the user types in the search term.\n * Defaults to `200`.\n */\n searchTypingDelay?: number;\n\n /**\n * The maximum number of results shown per group.\n * Defaults to `5`.\n */\n maxResultsPerGroup?: number;\n\n /**\n * This event handler will be called when the user selects a search result.\n */\n onSelect?: (event: SearchSelectEvent) => void;\n\n /**\n * This event handler will be called when the user clears the search input.\n */\n onClear?: () => void;\n}\n\n/**\n * A component that allows the user to search a given set of {@link SearchSource | SearchSources}.\n */\nexport const Search: FC<SearchProps> = (props) => {\n const { mapId, sources, searchTypingDelay, maxResultsPerGroup, onSelect, onClear } = props;\n const { containerProps } = useCommonComponentProps(\"search\", props);\n const { map } = useMapModel(mapId);\n const intl = useIntl();\n const controller = useController(sources, searchTypingDelay, maxResultsPerGroup, map);\n const { input, search, selectedOption, onInputChanged, onResultConfirmed } =\n useSearchState(controller);\n\n const chakraStyles = useChakraStyles();\n const ariaMessages = useAriaMessages(intl);\n const components = useCustomComponents();\n\n const handleInputChange = useEvent((newValue: string, actionMeta: InputActionMeta) => {\n // Only update the input if the user actually typed something.\n // This keeps the input content if the user focuses another element or if the menu is closed.\n if (actionMeta.action === \"input-change\") {\n onInputChanged(newValue);\n }\n });\n\n const handleSelectChange = useEvent(\n (value: SingleValue<SearchOption>, actionMeta: ActionMeta<SearchOption>) => {\n switch (actionMeta.action) {\n case \"select-option\":\n if (value) {\n // Updates the input field with the option label\n onResultConfirmed(value);\n onSelect?.({\n source: value.source,\n result: value.result\n });\n }\n break;\n case \"clear\":\n // Updates the input field\n onInputChanged(\"\");\n\n // the next two lines are a workaround for the open bug in react-select regarding the\n // cursor not being shown after clearing, although the component is focussed:\n // https://github.com/JedWatson/react-select/issues/3871\n selectRef.current?.blur();\n selectRef.current?.focus();\n onClear?.();\n break;\n default:\n LOG.debug(`Unhandled action type '${actionMeta.action}'.`);\n break;\n }\n }\n );\n\n const selectRef = useRef<SelectInstance<SearchOption, false, SearchGroupOption>>(null);\n return (\n <Box {...containerProps}>\n <Select<SearchOption, false, SearchGroupOption>\n className=\"search-component\"\n classNamePrefix=\"react-select\"\n ref={selectRef}\n inputValue={input}\n onInputChange={handleInputChange}\n aria-label={intl.formatMessage({ id: \"ariaLabel.search\" })}\n ariaLiveMessages={ariaMessages}\n colorScheme=\"trails\"\n selectedOptionStyle=\"color\"\n selectedOptionColorScheme=\"trails\"\n chakraStyles={chakraStyles}\n isClearable={true}\n placeholder={intl.formatMessage({ id: \"searchPlaceholder\" })}\n closeMenuOnSelect={true}\n isLoading={search.kind === \"loading\"}\n options={search.kind === \"ready\" ? search.results : undefined}\n filterOption={() => true} // always show all options (don't filter based on input text)\n tabSelectsValue={false}\n components={components}\n onChange={handleSelectChange}\n value={selectedOption}\n />\n </Box>\n );\n};\n\n/**\n * Provides custom aria messages for the select component.\n */\nfunction useAriaMessages(\n intl: PackageIntl\n): AriaLiveMessages<SearchOption, false, SearchGroupOption> {\n return useMemo(() => {\n /**\n * Method to create Aria-String for focus-Event\n */\n const onFocus: AriaOnFocus<SearchOption> = ({ focused }) => {\n return `${focused.label} ${intl.formatMessage({ id: \"ariaLabel.searchFocus\" })}.`;\n };\n\n /**\n * Method to create Aria-String for value-change-Event\n */\n const onChange: AriaOnChange<SearchOption, boolean> = ({ action, label }) => {\n let message = \"\";\n switch (action) {\n case \"select-option\":\n message = `${label} ${intl.formatMessage({ id: \"ariaLabel.searchSelect\" })}.`;\n break;\n case \"clear\":\n message = `${label} ${intl.formatMessage({ id: \"ariaLabel.searchClear\" })}.`;\n break;\n default:\n break;\n }\n return message;\n };\n\n /**\n * Method to create Aria-String for instruction\n */\n const guidance = () => {\n return `${intl.formatMessage({ id: \"ariaLabel.instructions\" })}`;\n };\n\n /**\n * Method to create Aria-String for result length\n */\n const onFilter = () => {\n return \"\";\n };\n\n return {\n onFocus,\n onChange,\n guidance,\n onFilter\n };\n }, [intl]);\n}\n\n/**\n * Customizes the inner components used by the select component.\n */\nfunction useCustomComponents(): SelectProps<SearchOption, false, SearchGroupOption>[\"components\"] {\n return useMemo(() => {\n return {\n Menu: MenuComp,\n Input: Input,\n SingleValue: SingleValueComp,\n Option: HighlightOption,\n NoOptionsMessage: NoOptionsMessage,\n LoadingMessage: LoadingMessage,\n ValueContainer: ValueContainer,\n IndicatorsContainer: IndicatorsContainer,\n ClearIndicator: ClearIndicator\n };\n }, []);\n}\n\n/**\n * Customizes components styles within the select component.\n */\nfunction useChakraStyles() {\n const [groupHeadingBg, focussedItemBg] = useToken(\n \"colors\",\n [\"trails.100\", \"trails.50\"],\n [\"#d5e5ec\", \"#eaf2f5\"]\n );\n return useMemo(() => {\n const chakraStyles: ChakraStylesConfig<SearchOption, false, SearchGroupOption> = {\n groupHeading: (provided) => ({\n ...provided,\n backgroundColor: groupHeadingBg,\n padding: \"8px 12px\",\n // make Header look like normal options:\n fontSize: \"inherit\",\n fontWeight: \"inherit\"\n }),\n option: (provided) => ({\n ...provided,\n backgroundColor: \"inherit\",\n _focus: {\n backgroundColor: focussedItemBg\n }\n }),\n dropdownIndicator: (provided) => ({\n ...provided,\n display: \"none\" // always hide\n })\n };\n return chakraStyles;\n }, [groupHeadingBg, focussedItemBg]);\n}\n\n/**\n * Creates a controller to search on the given sources.\n */\nfunction useController(\n sources: SearchSource[],\n searchTypingDelay: number | undefined,\n maxResultsPerGroup: number | undefined,\n map: MapModel | undefined\n) {\n const [controller, setController] = useState<SearchController | undefined>(undefined);\n useEffect(() => {\n if (!map) {\n return;\n }\n const controller = new SearchController(map, sources);\n setController(controller);\n return () => {\n controller.destroy();\n setController(undefined);\n };\n }, [map, sources]);\n\n useEffect(() => {\n controller && (controller.searchTypingDelay = searchTypingDelay);\n }, [controller, searchTypingDelay]);\n useEffect(() => {\n controller && (controller.maxResultsPerSource = maxResultsPerGroup);\n }, [controller, maxResultsPerGroup]);\n return controller;\n}\n\ntype SearchResultsReady = {\n kind: \"ready\";\n results: SearchGroupOption[];\n};\n\ntype SearchResultsLoading = {\n kind: \"loading\";\n};\n\ntype SearchResultsState = SearchResultsReady | SearchResultsLoading;\n\n/**\n * Keeps track of the current input text, active searches and their results.\n *\n * NOTE: it would be great to merge this state handling with the search controller\n * in a future revision.\n */\nfunction useSearchState(controller: SearchController | undefined) {\n interface FullSearchState {\n query: string;\n selectedOption: SearchOption | null;\n search: SearchResultsState;\n }\n\n type Action =\n | { kind: \"input\"; query: string }\n | { kind: \"select-option\"; option: SearchOption }\n | { kind: \"load-results\" }\n | { kind: \"accept-results\"; results: SearchGroupOption[] };\n\n const [state, dispatch] = useReducer(\n (current: FullSearchState, action: Action): FullSearchState => {\n switch (action.kind) {\n case \"input\":\n return {\n ...current,\n query: action.query,\n selectedOption: null\n };\n case \"select-option\":\n return {\n ...current,\n selectedOption: action.option,\n query: action.option.label\n };\n case \"load-results\":\n return {\n ...current,\n search: {\n kind: \"loading\"\n }\n };\n case \"accept-results\":\n return {\n ...current,\n search: {\n kind: \"ready\",\n results: action.results\n }\n };\n }\n },\n undefined,\n (): FullSearchState => ({\n query: \"\",\n selectedOption: null,\n search: {\n kind: \"ready\",\n results: []\n }\n })\n );\n\n // Stores the promise for the current search.\n // Any results from outdated searches are ignored.\n const currentSearch = useRef<Promise<unknown>>();\n const startSearch = useEvent((query: string) => {\n if (!controller) {\n currentSearch.current = undefined;\n dispatch({ kind: \"accept-results\", results: [] });\n return;\n }\n\n LOG.isDebug() && LOG.debug(`Starting new search for query ${JSON.stringify(query)}.`);\n dispatch({ kind: \"load-results\" });\n const promise = (currentSearch.current = search(controller, query).then((results) => {\n // Check if this job is still current\n if (currentSearch.current === promise) {\n dispatch({ kind: \"accept-results\", results });\n }\n }));\n });\n\n // Called when the user confirms a search result\n const onResultConfirmed = useCallback((option: SearchOption) => {\n // Do not start a new search when the user confirms a result\n dispatch({ kind: \"select-option\", option });\n }, []);\n\n // Called when a user types into the input field\n const onInputChanged = useCallback(\n (newValue: string) => {\n // Trigger a new search if the user changes the query by typing\n dispatch({ kind: \"input\", query: newValue });\n startSearch(newValue);\n },\n [startSearch]\n );\n\n return {\n input: state.query,\n search: state.search,\n selectedOption: state.selectedOption,\n onResultConfirmed,\n onInputChanged\n };\n}\n\nasync function search(controller: SearchController, query: string): Promise<SearchGroupOption[]> {\n let suggestions: SuggestionGroup[];\n try {\n suggestions = await controller.search(query);\n } catch (error) {\n if (!isAbortError(error)) {\n LOG.error(`Search failed`, error);\n }\n suggestions = [];\n }\n return mapSuggestions(suggestions);\n}\n\nfunction mapSuggestions(suggestions: SuggestionGroup[]): SearchGroupOption[] {\n const options = suggestions.map(\n (group, groupIndex): SearchGroupOption => ({\n label: group.label,\n options: group.results.map((suggestion): SearchOption => {\n return {\n value: `${groupIndex}-${suggestion.id}`,\n label: suggestion.label,\n source: group.source,\n result: suggestion\n };\n })\n })\n );\n return options;\n}\n"],"names":["search","SingleValueComp","controller"],"mappings":";;;;;;;;;;;AAmCA,MAAM,GAAA,GAAM,aAAa,eAAe,CAAA,CAAA;AA2E3B,MAAA,MAAA,GAA0B,CAAC,KAAU,KAAA;AAC9C,EAAA,MAAM,EAAE,KAAO,EAAA,OAAA,EAAS,mBAAmB,kBAAoB,EAAA,QAAA,EAAU,SAAY,GAAA,KAAA,CAAA;AACrF,EAAA,MAAM,EAAE,cAAA,EAAmB,GAAA,uBAAA,CAAwB,UAAU,KAAK,CAAA,CAAA;AAClE,EAAA,MAAM,EAAE,GAAA,EAAQ,GAAA,WAAA,CAAY,KAAK,CAAA,CAAA;AACjC,EAAA,MAAM,OAAO,OAAQ,EAAA,CAAA;AACrB,EAAA,MAAM,UAAa,GAAA,aAAA,CAAc,OAAS,EAAA,iBAAA,EAAmB,oBAAoB,GAAG,CAAA,CAAA;AACpF,EAAM,MAAA,EAAE,OAAO,MAAAA,EAAAA,OAAAA,EAAQ,gBAAgB,cAAgB,EAAA,iBAAA,EACnD,GAAA,cAAA,CAAe,UAAU,CAAA,CAAA;AAE7B,EAAA,MAAM,eAAe,eAAgB,EAAA,CAAA;AACrC,EAAM,MAAA,YAAA,GAAe,gBAAgB,IAAI,CAAA,CAAA;AACzC,EAAA,MAAM,aAAa,mBAAoB,EAAA,CAAA;AAEvC,EAAA,MAAM,iBAAoB,GAAA,QAAA,CAAS,CAAC,QAAA,EAAkB,UAAgC,KAAA;AAGlF,IAAI,IAAA,UAAA,CAAW,WAAW,cAAgB,EAAA;AACtC,MAAA,cAAA,CAAe,QAAQ,CAAA,CAAA;AAAA,KAC3B;AAAA,GACH,CAAA,CAAA;AAED,EAAA,MAAM,kBAAqB,GAAA,QAAA;AAAA,IACvB,CAAC,OAAkC,UAAyC,KAAA;AACxE,MAAA,QAAQ,WAAW,MAAQ;AAAA,QACvB,KAAK,eAAA;AACD,UAAA,IAAI,KAAO,EAAA;AAEP,YAAA,iBAAA,CAAkB,KAAK,CAAA,CAAA;AACvB,YAAW,QAAA,GAAA;AAAA,cACP,QAAQ,KAAM,CAAA,MAAA;AAAA,cACd,QAAQ,KAAM,CAAA,MAAA;AAAA,aACjB,CAAA,CAAA;AAAA,WACL;AACA,UAAA,MAAA;AAAA,QACJ,KAAK,OAAA;AAED,UAAA,cAAA,CAAe,EAAE,CAAA,CAAA;AAKjB,UAAA,SAAA,CAAU,SAAS,IAAK,EAAA,CAAA;AACxB,UAAA,SAAA,CAAU,SAAS,KAAM,EAAA,CAAA;AACzB,UAAU,OAAA,IAAA,CAAA;AACV,UAAA,MAAA;AAAA,QACJ;AACI,UAAA,GAAA,CAAI,KAAM,CAAA,CAAA,uBAAA,EAA0B,UAAW,CAAA,MAAM,CAAI,EAAA,CAAA,CAAA,CAAA;AACzD,UAAA,MAAA;AAAA,OACR;AAAA,KACJ;AAAA,GACJ,CAAA;AAEA,EAAM,MAAA,SAAA,GAAY,OAA+D,IAAI,CAAA,CAAA;AACrF,EACI,uBAAA,GAAA,CAAC,GAAK,EAAA,EAAA,GAAG,cACL,EAAA,QAAA,kBAAA,GAAA;AAAA,IAAC,MAAA;AAAA,IAAA;AAAA,MACG,SAAU,EAAA,kBAAA;AAAA,MACV,eAAgB,EAAA,cAAA;AAAA,MAChB,GAAK,EAAA,SAAA;AAAA,MACL,UAAY,EAAA,KAAA;AAAA,MACZ,aAAe,EAAA,iBAAA;AAAA,MACf,cAAY,IAAK,CAAA,aAAA,CAAc,EAAE,EAAA,EAAI,oBAAoB,CAAA;AAAA,MACzD,gBAAkB,EAAA,YAAA;AAAA,MAClB,WAAY,EAAA,QAAA;AAAA,MACZ,mBAAoB,EAAA,OAAA;AAAA,MACpB,yBAA0B,EAAA,QAAA;AAAA,MAC1B,YAAA;AAAA,MACA,WAAa,EAAA,IAAA;AAAA,MACb,aAAa,IAAK,CAAA,aAAA,CAAc,EAAE,EAAA,EAAI,qBAAqB,CAAA;AAAA,MAC3D,iBAAmB,EAAA,IAAA;AAAA,MACnB,SAAA,EAAWA,QAAO,IAAS,KAAA,SAAA;AAAA,MAC3B,OAASA,EAAAA,OAAAA,CAAO,IAAS,KAAA,OAAA,GAAUA,QAAO,OAAU,GAAA,KAAA,CAAA;AAAA,MACpD,cAAc,MAAM,IAAA;AAAA,MACpB,eAAiB,EAAA,KAAA;AAAA,MACjB,UAAA;AAAA,MACA,QAAU,EAAA,kBAAA;AAAA,MACV,KAAO,EAAA,cAAA;AAAA,KAAA;AAAA,GAEf,EAAA,CAAA,CAAA;AAER,EAAA;AAKA,SAAS,gBACL,IACwD,EAAA;AACxD,EAAA,OAAO,QAAQ,MAAM;AAIjB,IAAA,MAAM,OAAqC,GAAA,CAAC,EAAE,OAAA,EAAc,KAAA;AACxD,MAAO,OAAA,CAAA,EAAG,OAAQ,CAAA,KAAK,CAAI,CAAA,EAAA,IAAA,CAAK,cAAc,EAAE,EAAA,EAAI,uBAAwB,EAAC,CAAC,CAAA,CAAA,CAAA,CAAA;AAAA,KAClF,CAAA;AAKA,IAAA,MAAM,QAAgD,GAAA,CAAC,EAAE,MAAA,EAAQ,OAAY,KAAA;AACzE,MAAA,IAAI,OAAU,GAAA,EAAA,CAAA;AACd,MAAA,QAAQ,MAAQ;AAAA,QACZ,KAAK,eAAA;AACD,UAAU,OAAA,GAAA,CAAA,EAAG,KAAK,CAAI,CAAA,EAAA,IAAA,CAAK,cAAc,EAAE,EAAA,EAAI,wBAAyB,EAAC,CAAC,CAAA,CAAA,CAAA,CAAA;AAC1E,UAAA,MAAA;AAAA,QACJ,KAAK,OAAA;AACD,UAAU,OAAA,GAAA,CAAA,EAAG,KAAK,CAAI,CAAA,EAAA,IAAA,CAAK,cAAc,EAAE,EAAA,EAAI,uBAAwB,EAAC,CAAC,CAAA,CAAA,CAAA,CAAA;AACzE,UAAA,MAAA;AAEA,OACR;AACA,MAAO,OAAA,OAAA,CAAA;AAAA,KACX,CAAA;AAKA,IAAA,MAAM,WAAW,MAAM;AACnB,MAAA,OAAO,GAAG,IAAK,CAAA,aAAA,CAAc,EAAE,EAAI,EAAA,wBAAA,EAA0B,CAAC,CAAA,CAAA,CAAA;AAAA,KAClE,CAAA;AAKA,IAAA,MAAM,WAAW,MAAM;AACnB,MAAO,OAAA,EAAA,CAAA;AAAA,KACX,CAAA;AAEA,IAAO,OAAA;AAAA,MACH,OAAA;AAAA,MACA,QAAA;AAAA,MACA,QAAA;AAAA,MACA,QAAA;AAAA,KACJ,CAAA;AAAA,GACJ,EAAG,CAAC,IAAI,CAAC,CAAA,CAAA;AACb,CAAA;AAKA,SAAS,mBAAyF,GAAA;AAC9F,EAAA,OAAO,QAAQ,MAAM;AACjB,IAAO,OAAA;AAAA,MACH,IAAM,EAAA,QAAA;AAAA,MACN,KAAA;AAAA,MACA,WAAa,EAAAC,WAAA;AAAA,MACb,MAAQ,EAAA,eAAA;AAAA,MACR,gBAAA;AAAA,MACA,cAAA;AAAA,MACA,cAAA;AAAA,MACA,mBAAA;AAAA,MACA,cAAA;AAAA,KACJ,CAAA;AAAA,GACJ,EAAG,EAAE,CAAA,CAAA;AACT,CAAA;AAKA,SAAS,eAAkB,GAAA;AACvB,EAAM,MAAA,CAAC,cAAgB,EAAA,cAAc,CAAI,GAAA,QAAA;AAAA,IACrC,QAAA;AAAA,IACA,CAAC,cAAc,WAAW,CAAA;AAAA,IAC1B,CAAC,WAAW,SAAS,CAAA;AAAA,GACzB,CAAA;AACA,EAAA,OAAO,QAAQ,MAAM;AACjB,IAAA,MAAM,YAA2E,GAAA;AAAA,MAC7E,YAAA,EAAc,CAAC,QAAc,MAAA;AAAA,QACzB,GAAG,QAAA;AAAA,QACH,eAAiB,EAAA,cAAA;AAAA,QACjB,OAAS,EAAA,UAAA;AAAA;AAAA,QAET,QAAU,EAAA,SAAA;AAAA,QACV,UAAY,EAAA,SAAA;AAAA,OAChB,CAAA;AAAA,MACA,MAAA,EAAQ,CAAC,QAAc,MAAA;AAAA,QACnB,GAAG,QAAA;AAAA,QACH,eAAiB,EAAA,SAAA;AAAA,QACjB,MAAQ,EAAA;AAAA,UACJ,eAAiB,EAAA,cAAA;AAAA,SACrB;AAAA,OACJ,CAAA;AAAA,MACA,iBAAA,EAAmB,CAAC,QAAc,MAAA;AAAA,QAC9B,GAAG,QAAA;AAAA,QACH,OAAS,EAAA,MAAA;AAAA;AAAA,OACb,CAAA;AAAA,KACJ,CAAA;AACA,IAAO,OAAA,YAAA,CAAA;AAAA,GACR,EAAA,CAAC,cAAgB,EAAA,cAAc,CAAC,CAAA,CAAA;AACvC,CAAA;AAKA,SAAS,aACL,CAAA,OAAA,EACA,iBACA,EAAA,kBAAA,EACA,GACF,EAAA;AACE,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,SAAuC,KAAS,CAAA,CAAA,CAAA;AACpF,EAAA,SAAA,CAAU,MAAM;AACZ,IAAA,IAAI,CAAC,GAAK,EAAA;AACN,MAAA,OAAA;AAAA,KACJ;AACA,IAAA,MAAMC,WAAa,GAAA,IAAI,gBAAiB,CAAA,GAAA,EAAK,OAAO,CAAA,CAAA;AACpD,IAAA,aAAA,CAAcA,WAAU,CAAA,CAAA;AACxB,IAAA,OAAO,MAAM;AACT,MAAAA,YAAW,OAAQ,EAAA,CAAA;AACnB,MAAA,aAAA,CAAc,KAAS,CAAA,CAAA,CAAA;AAAA,KAC3B,CAAA;AAAA,GACD,EAAA,CAAC,GAAK,EAAA,OAAO,CAAC,CAAA,CAAA;AAEjB,EAAA,SAAA,CAAU,MAAM;AACZ,IAAA,UAAA,KAAe,WAAW,iBAAoB,GAAA,iBAAA,CAAA,CAAA;AAAA,GAC/C,EAAA,CAAC,UAAY,EAAA,iBAAiB,CAAC,CAAA,CAAA;AAClC,EAAA,SAAA,CAAU,MAAM;AACZ,IAAA,UAAA,KAAe,WAAW,mBAAsB,GAAA,kBAAA,CAAA,CAAA;AAAA,GACjD,EAAA,CAAC,UAAY,EAAA,kBAAkB,CAAC,CAAA,CAAA;AACnC,EAAO,OAAA,UAAA,CAAA;AACX,CAAA;AAmBA,SAAS,eAAe,UAA0C,EAAA;AAa9D,EAAM,MAAA,CAAC,KAAO,EAAA,QAAQ,CAAI,GAAA,UAAA;AAAA,IACtB,CAAC,SAA0B,MAAoC,KAAA;AAC3D,MAAA,QAAQ,OAAO,IAAM;AAAA,QACjB,KAAK,OAAA;AACD,UAAO,OAAA;AAAA,YACH,GAAG,OAAA;AAAA,YACH,OAAO,MAAO,CAAA,KAAA;AAAA,YACd,cAAgB,EAAA,IAAA;AAAA,WACpB,CAAA;AAAA,QACJ,KAAK,eAAA;AACD,UAAO,OAAA;AAAA,YACH,GAAG,OAAA;AAAA,YACH,gBAAgB,MAAO,CAAA,MAAA;AAAA,YACvB,KAAA,EAAO,OAAO,MAAO,CAAA,KAAA;AAAA,WACzB,CAAA;AAAA,QACJ,KAAK,cAAA;AACD,UAAO,OAAA;AAAA,YACH,GAAG,OAAA;AAAA,YACH,MAAQ,EAAA;AAAA,cACJ,IAAM,EAAA,SAAA;AAAA,aACV;AAAA,WACJ,CAAA;AAAA,QACJ,KAAK,gBAAA;AACD,UAAO,OAAA;AAAA,YACH,GAAG,OAAA;AAAA,YACH,MAAQ,EAAA;AAAA,cACJ,IAAM,EAAA,OAAA;AAAA,cACN,SAAS,MAAO,CAAA,OAAA;AAAA,aACpB;AAAA,WACJ,CAAA;AAAA,OACR;AAAA,KACJ;AAAA,IACA,KAAA,CAAA;AAAA,IACA,OAAwB;AAAA,MACpB,KAAO,EAAA,EAAA;AAAA,MACP,cAAgB,EAAA,IAAA;AAAA,MAChB,MAAQ,EAAA;AAAA,QACJ,IAAM,EAAA,OAAA;AAAA,QACN,SAAS,EAAC;AAAA,OACd;AAAA,KACJ,CAAA;AAAA,GACJ,CAAA;AAIA,EAAA,MAAM,gBAAgB,MAAyB,EAAA,CAAA;AAC/C,EAAM,MAAA,WAAA,GAAc,QAAS,CAAA,CAAC,KAAkB,KAAA;AAC5C,IAAA,IAAI,CAAC,UAAY,EAAA;AACb,MAAA,aAAA,CAAc,OAAU,GAAA,KAAA,CAAA,CAAA;AACxB,MAAA,QAAA,CAAS,EAAE,IAAM,EAAA,gBAAA,EAAkB,OAAS,EAAA,IAAI,CAAA,CAAA;AAChD,MAAA,OAAA;AAAA,KACJ;AAEA,IAAI,GAAA,CAAA,OAAA,MAAa,GAAI,CAAA,KAAA,CAAM,iCAAiC,IAAK,CAAA,SAAA,CAAU,KAAK,CAAC,CAAG,CAAA,CAAA,CAAA,CAAA;AACpF,IAAS,QAAA,CAAA,EAAE,IAAM,EAAA,cAAA,EAAgB,CAAA,CAAA;AACjC,IAAM,MAAA,OAAA,GAAW,cAAc,OAAU,GAAA,MAAA,CAAO,YAAY,KAAK,CAAA,CAAE,IAAK,CAAA,CAAC,OAAY,KAAA;AAEjF,MAAI,IAAA,aAAA,CAAc,YAAY,OAAS,EAAA;AACnC,QAAA,QAAA,CAAS,EAAE,IAAA,EAAM,gBAAkB,EAAA,OAAA,EAAS,CAAA,CAAA;AAAA,OAChD;AAAA,KACH,CAAA,CAAA;AAAA,GACJ,CAAA,CAAA;AAGD,EAAM,MAAA,iBAAA,GAAoB,WAAY,CAAA,CAAC,MAAyB,KAAA;AAE5D,IAAA,QAAA,CAAS,EAAE,IAAA,EAAM,eAAiB,EAAA,MAAA,EAAQ,CAAA,CAAA;AAAA,GAC9C,EAAG,EAAE,CAAA,CAAA;AAGL,EAAA,MAAM,cAAiB,GAAA,WAAA;AAAA,IACnB,CAAC,QAAqB,KAAA;AAElB,MAAA,QAAA,CAAS,EAAE,IAAA,EAAM,OAAS,EAAA,KAAA,EAAO,UAAU,CAAA,CAAA;AAC3C,MAAA,WAAA,CAAY,QAAQ,CAAA,CAAA;AAAA,KACxB;AAAA,IACA,CAAC,WAAW,CAAA;AAAA,GAChB,CAAA;AAEA,EAAO,OAAA;AAAA,IACH,OAAO,KAAM,CAAA,KAAA;AAAA,IACb,QAAQ,KAAM,CAAA,MAAA;AAAA,IACd,gBAAgB,KAAM,CAAA,cAAA;AAAA,IACtB,iBAAA;AAAA,IACA,cAAA;AAAA,GACJ,CAAA;AACJ,CAAA;AAEA,eAAe,MAAA,CAAO,YAA8B,KAA6C,EAAA;AAC7F,EAAI,IAAA,WAAA,CAAA;AACJ,EAAI,IAAA;AACA,IAAc,WAAA,GAAA,MAAM,UAAW,CAAA,MAAA,CAAO,KAAK,CAAA,CAAA;AAAA,WACtC,KAAO,EAAA;AACZ,IAAI,IAAA,CAAC,YAAa,CAAA,KAAK,CAAG,EAAA;AACtB,MAAI,GAAA,CAAA,KAAA,CAAM,iBAAiB,KAAK,CAAA,CAAA;AAAA,KACpC;AACA,IAAA,WAAA,GAAc,EAAC,CAAA;AAAA,GACnB;AACA,EAAA,OAAO,eAAe,WAAW,CAAA,CAAA;AACrC,CAAA;AAEA,SAAS,eAAe,WAAqD,EAAA;AACzE,EAAA,MAAM,UAAU,WAAY,CAAA,GAAA;AAAA,IACxB,CAAC,OAAO,UAAmC,MAAA;AAAA,MACvC,OAAO,KAAM,CAAA,KAAA;AAAA,MACb,OAAS,EAAA,KAAA,CAAM,OAAQ,CAAA,GAAA,CAAI,CAAC,UAA6B,KAAA;AACrD,QAAO,OAAA;AAAA,UACH,KAAO,EAAA,CAAA,EAAG,UAAU,CAAA,CAAA,EAAI,WAAW,EAAE,CAAA,CAAA;AAAA,UACrC,OAAO,UAAW,CAAA,KAAA;AAAA,UAClB,QAAQ,KAAM,CAAA,MAAA;AAAA,UACd,MAAQ,EAAA,UAAA;AAAA,SACZ,CAAA;AAAA,OACH,CAAA;AAAA,KACL,CAAA;AAAA,GACJ,CAAA;AACA,EAAO,OAAA,OAAA,CAAA;AACX;;;;"}
@@ -0,0 +1,21 @@
1
+ import { SearchSource, SearchResult } from "./api";
2
+ import { MapModel } from "@open-pioneer/map";
3
+ /**
4
+ * Group of suggestions returned from one source.
5
+ */
6
+ export interface SuggestionGroup {
7
+ label: string;
8
+ source: SearchSource;
9
+ results: SearchResult[];
10
+ }
11
+ export declare class SearchController {
12
+ #private;
13
+ constructor(mapModel: MapModel, sources: SearchSource[]);
14
+ destroy(): void;
15
+ search(searchTerm: string): Promise<SuggestionGroup[]>;
16
+ get searchTypingDelay(): number;
17
+ set searchTypingDelay(value: number | undefined);
18
+ get maxResultsPerSource(): number;
19
+ set maxResultsPerSource(value: number | undefined);
20
+ get sources(): SearchSource[];
21
+ }
@@ -0,0 +1,108 @@
1
+ import { createLogger, throwAbortError, isAbortError } from '@open-pioneer/core';
2
+
3
+ const LOG = createLogger("search:SearchController");
4
+ const DEFAULT_SEARCH_TYPING_DELAY = 200;
5
+ const DEFAULT_MAX_RESULTS_PER_SOURCE = 5;
6
+ class SearchController {
7
+ #mapModel;
8
+ /**
9
+ * Search sources defined by the developer.
10
+ */
11
+ #sources = [];
12
+ /**
13
+ * Limits the number of results per source.
14
+ */
15
+ #maxResultsPerSource = DEFAULT_MAX_RESULTS_PER_SOURCE;
16
+ /**
17
+ * The timeout in millis.
18
+ */
19
+ #searchTypingDelay = DEFAULT_SEARCH_TYPING_DELAY;
20
+ /**
21
+ * Cancel or abort a previous request.
22
+ */
23
+ #abortController;
24
+ constructor(mapModel, sources) {
25
+ this.#mapModel = mapModel;
26
+ this.#sources = sources;
27
+ }
28
+ destroy() {
29
+ this.#abortController?.abort();
30
+ this.#abortController = void 0;
31
+ }
32
+ async search(searchTerm) {
33
+ this.#abortController?.abort();
34
+ this.#abortController = void 0;
35
+ if (!searchTerm) {
36
+ return [];
37
+ }
38
+ const abort = this.#abortController = new AbortController();
39
+ try {
40
+ await waitForTimeOut(abort.signal, this.#searchTypingDelay);
41
+ if (abort.signal.aborted) {
42
+ LOG.debug(`search canceled with ${searchTerm}`);
43
+ throwAbortError();
44
+ }
45
+ const settledSearches = await Promise.all(
46
+ this.#sources.map((source) => this.#searchSource(source, searchTerm, abort.signal))
47
+ );
48
+ return settledSearches.filter((s) => s != null);
49
+ } finally {
50
+ if (this.#abortController === abort) {
51
+ this.#abortController = void 0;
52
+ }
53
+ }
54
+ }
55
+ async #searchSource(source, searchTerm, signal) {
56
+ const label = source.label;
57
+ const projection = this.#mapModel.olMap.getView().getProjection();
58
+ try {
59
+ const maxResults = this.#maxResultsPerSource;
60
+ let results = await source.search(searchTerm, {
61
+ maxResults,
62
+ signal,
63
+ mapProjection: projection
64
+ });
65
+ if (results.length > maxResults) {
66
+ results = results.slice(0, maxResults);
67
+ }
68
+ return { label, source, results };
69
+ } catch (e) {
70
+ if (!isAbortError(e)) {
71
+ LOG.error(`search for source ${label} failed`, e);
72
+ }
73
+ return void 0;
74
+ }
75
+ }
76
+ get searchTypingDelay() {
77
+ return this.#searchTypingDelay;
78
+ }
79
+ set searchTypingDelay(value) {
80
+ this.#searchTypingDelay = value ?? DEFAULT_SEARCH_TYPING_DELAY;
81
+ }
82
+ get maxResultsPerSource() {
83
+ return this.#maxResultsPerSource;
84
+ }
85
+ set maxResultsPerSource(value) {
86
+ this.#maxResultsPerSource = value ?? DEFAULT_MAX_RESULTS_PER_SOURCE;
87
+ }
88
+ get sources() {
89
+ return this.#sources;
90
+ }
91
+ }
92
+ async function waitForTimeOut(signal, timeoutMillis) {
93
+ if (signal.aborted) {
94
+ return;
95
+ }
96
+ await new Promise((resolve) => {
97
+ const done = () => {
98
+ signal.removeEventListener("abort", done);
99
+ clearTimeout(timeoutId);
100
+ resolve();
101
+ };
102
+ signal.addEventListener("abort", done);
103
+ const timeoutId = setTimeout(done, timeoutMillis);
104
+ });
105
+ }
106
+
107
+ export { SearchController };
108
+ //# sourceMappingURL=SearchController.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SearchController.js","sources":["SearchController.ts"],"sourcesContent":["// SPDX-FileCopyrightText: 2023 Open Pioneer project (https://github.com/open-pioneer)\n// SPDX-License-Identifier: Apache-2.0\nimport { createLogger, isAbortError, throwAbortError } from \"@open-pioneer/core\";\nimport { SearchSource, SearchResult } from \"./api\";\nimport { MapModel } from \"@open-pioneer/map\";\n\nconst LOG = createLogger(\"search:SearchController\");\n\n/**\n * Group of suggestions returned from one source.\n */\nexport interface SuggestionGroup {\n label: string;\n source: SearchSource;\n results: SearchResult[];\n}\n\nconst DEFAULT_SEARCH_TYPING_DELAY = 200;\nconst DEFAULT_MAX_RESULTS_PER_SOURCE = 5;\n\nexport class SearchController {\n #mapModel: MapModel;\n\n /**\n * Search sources defined by the developer.\n */\n #sources: SearchSource[] = [];\n\n /**\n * Limits the number of results per source.\n */\n #maxResultsPerSource: number = DEFAULT_MAX_RESULTS_PER_SOURCE;\n\n /**\n * The timeout in millis.\n */\n #searchTypingDelay: number = DEFAULT_SEARCH_TYPING_DELAY;\n\n /**\n * Cancel or abort a previous request.\n */\n #abortController: AbortController | undefined;\n\n constructor(mapModel: MapModel, sources: SearchSource[]) {\n this.#mapModel = mapModel;\n this.#sources = sources;\n }\n\n destroy() {\n this.#abortController?.abort();\n this.#abortController = undefined;\n }\n\n async search(searchTerm: string): Promise<SuggestionGroup[]> {\n this.#abortController?.abort();\n this.#abortController = undefined;\n if (!searchTerm) {\n return [];\n }\n\n const abort = (this.#abortController = new AbortController());\n try {\n await waitForTimeOut(abort.signal, this.#searchTypingDelay);\n if (abort.signal.aborted) {\n LOG.debug(`search canceled with ${searchTerm}`);\n throwAbortError();\n }\n const settledSearches = await Promise.all(\n this.#sources.map((source) => this.#searchSource(source, searchTerm, abort.signal))\n );\n return settledSearches.filter((s): s is SuggestionGroup => s != null);\n } finally {\n if (this.#abortController === abort) {\n this.#abortController = undefined;\n }\n }\n }\n\n async #searchSource(\n source: SearchSource,\n searchTerm: string,\n signal: AbortSignal\n ): Promise<SuggestionGroup | undefined> {\n const label = source.label;\n const projection = this.#mapModel.olMap.getView().getProjection();\n try {\n const maxResults = this.#maxResultsPerSource;\n let results = await source.search(searchTerm, {\n maxResults,\n signal,\n mapProjection: projection\n });\n if (results.length > maxResults) {\n results = results.slice(0, maxResults);\n }\n return { label, source, results };\n } catch (e) {\n if (!isAbortError(e)) {\n LOG.error(`search for source ${label} failed`, e);\n }\n return undefined;\n }\n }\n\n get searchTypingDelay(): number {\n return this.#searchTypingDelay;\n }\n\n set searchTypingDelay(value: number | undefined) {\n this.#searchTypingDelay = value ?? DEFAULT_SEARCH_TYPING_DELAY;\n }\n\n get maxResultsPerSource(): number {\n return this.#maxResultsPerSource;\n }\n\n set maxResultsPerSource(value: number | undefined) {\n this.#maxResultsPerSource = value ?? DEFAULT_MAX_RESULTS_PER_SOURCE;\n }\n\n get sources() {\n return this.#sources;\n }\n}\n\n/**\n * wait for timeouts millis or until signal is aborted, whatever happens first\n */\nasync function waitForTimeOut(signal: AbortSignal, timeoutMillis: number) {\n if (signal.aborted) {\n return;\n }\n\n await new Promise<void>((resolve) => {\n const done = () => {\n signal.removeEventListener(\"abort\", done);\n clearTimeout(timeoutId);\n resolve();\n };\n\n signal.addEventListener(\"abort\", done);\n const timeoutId = setTimeout(done, timeoutMillis);\n });\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ import { useIntlInternal } from '@open-pioneer/runtime/react-integration';
2
+
3
+ const PACKAGE_NAME = "@open-pioneer/search";
4
+ const useIntl = /*@__PURE__*/ useIntlInternal.bind(undefined, PACKAGE_NAME);
5
+
6
+ export { useIntl };
7
+ //# sourceMappingURL=_virtual-pioneer-module_react-hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_virtual-pioneer-module_react-hooks.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;"}
package/api.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ import { Geometry } from "ol/geom";
2
+ import { Projection } from "ol/proj";
3
+ /**
4
+ * An object that allows searching some set of data.
5
+ *
6
+ * Developers can create classes that implement this interface for different search sources.
7
+ */
8
+ export interface SearchSource {
9
+ /**
10
+ * The label of this source.
11
+ *
12
+ * This will be displayed by the user interface when results from this search source are shown.
13
+ */
14
+ readonly label: string;
15
+ /**
16
+ * Performs a search and return a list of search results.
17
+ *
18
+ * Implementations should return the results ordered by priority (best match first), if possible.
19
+ *
20
+ * The provided `AbortSignal` in `options.signal` is used to cancel outdated requests.
21
+ *
22
+ * NOTE: If your search source implements custom error handling (i.e. `try`/`catch`), it is good practice to forward
23
+ * abort errors without modification. This will enable the Search widget to hide "errors" due to
24
+ * cancellation.
25
+ *
26
+ * For example:
27
+ *
28
+ * ```js
29
+ * import { isAbortError } from "@open-pioneer/core";
30
+ *
31
+ * class CustomSearchSource {
32
+ * async search(input, { signal }) {
33
+ * try {
34
+ * // If the search is cancelled by the UI, doRequest
35
+ * // will throw an AbortError. It might throw other errors
36
+ * // due to application errors, network problems etc.
37
+ * const result = await doCustomSearch(input, signal);
38
+ * // ... do something with result
39
+ * } catch (e) {
40
+ * if (isAbortError(e)) {
41
+ * throw e; // rethrow original error
42
+ * }
43
+ * // Possibly use custom error codes or error classes for better error messages
44
+ * throw new Error("Custom search failed", { cause: e });
45
+ * }
46
+ * }
47
+ * }
48
+ * ```
49
+ */
50
+ search(inputValue: string, options: SearchOptions): Promise<SearchResult[]>;
51
+ }
52
+ /** Options passed to a {@link SearchSource} when triggering a search. */
53
+ export interface SearchOptions {
54
+ /**
55
+ * The maximum number of search results requested by the search widget.
56
+ * The widget will not display additional results, should the source provide them.
57
+ *
58
+ * This property allows the source to fetch no more results than necessary.
59
+ */
60
+ maxResults: number;
61
+ /**
62
+ * The signal can be used to detect cancellation.
63
+ * The search widget will automatically cancel obsolete requests
64
+ * when new search operations are started.
65
+ *
66
+ * You can pass this signal to builtin functions like `fetch` that automatically
67
+ * support cancellation.
68
+ */
69
+ signal: AbortSignal;
70
+ /**
71
+ * The current projection of the map.
72
+ * Useful to return the search result's geometry in the suitable projection.
73
+ */
74
+ mapProjection: Projection;
75
+ }
76
+ /**
77
+ * Represent the result of a search.
78
+ */
79
+ export interface SearchResult {
80
+ /**
81
+ * Identifier for the result object.
82
+ * Values used here should be unique within the context of the search source that returns them.
83
+ *
84
+ * If your source cannot provide a useful id on its own, another strategy to generate unique ids is to
85
+ * generate a [UUID](https://www.npmjs.com/package/uuid#uuidv4options-buffer-offset) instead.
86
+ */
87
+ id: number | string;
88
+ /**
89
+ * Display text representing this result.
90
+ * Will be shown in the search widget's suggestion list.
91
+ */
92
+ label: string;
93
+ /**
94
+ * Optional geometry.
95
+ *
96
+ * If a geometry is provided, one should also specify the {@link projection}.
97
+ *
98
+ * If no projection has been specified, calling code should assume the map's projection.
99
+ */
100
+ geometry?: Geometry;
101
+ /**
102
+ * The projection of the {@link geometry}.
103
+ */
104
+ projection?: string;
105
+ /**
106
+ * Arbitrary additional properties.
107
+ */
108
+ properties?: Readonly<Record<string, unknown>>;
109
+ }
package/i18n/de.yaml ADDED
@@ -0,0 +1,11 @@
1
+ messages:
2
+ noOptionsText: "Keine Suchtreffer gefunden"
3
+ loadingText: "Frage Daten ab..."
4
+ searchPlaceholder: "Suche..."
5
+ ariaLabel:
6
+ search: "Suchleiste"
7
+ instructions: "Benutze die Pfeiltasten Hoch und Runter um durch die Suchergebnisse zu scrollen, drücke Enter um das Suchergebnis zu selektieren, drücke Escape um zurückzukehren"
8
+ searchFocus: "fokussiert"
9
+ searchSelect: "selektiert"
10
+ searchClear: "gelöscht"
11
+ clearButton: "Suche leeren"
package/i18n/en.yaml ADDED
@@ -0,0 +1,11 @@
1
+ messages:
2
+ noOptionsText: "No results found"
3
+ loadingText: "Loading..."
4
+ searchPlaceholder: "Search..."
5
+ ariaLabel:
6
+ search: "Search bar"
7
+ instructions: "Use Up and Down to choose options, press Enter to select the currently focused option, press Escape to exit the menu, press Tab to select the option and exit the menu."
8
+ searchFocus: "focused"
9
+ searchSelect: "selected"
10
+ searchClear: "cleared"
11
+ clearButton: "Empty the search"
package/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { Search, type SearchProps, type SearchSelectEvent } from "./Search";
2
+ export type { SearchSource, SearchResult, SearchOptions } from "./api";
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { Search } from './Search.js';
2
+ //# sourceMappingURL=index.js.map
package/index.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@open-pioneer/search",
4
+ "version": "0.1.0",
5
+ "license": "Apache-2.0",
6
+ "peerDependencies": {
7
+ "@open-pioneer/chakra-integration": "^1.1.1",
8
+ "@open-pioneer/runtime": "^2.0.2",
9
+ "@open-pioneer/core": "^1.2.1",
10
+ "ol": "^8.2.0",
11
+ "react": "^18.2.0",
12
+ "classnames": "^2.3.2",
13
+ "chakra-react-select": "^4.7.6",
14
+ "@chakra-ui/icons": "^2.1.1",
15
+ "@open-pioneer/map": "^0.1.1",
16
+ "@open-pioneer/react-utils": "^0.1.0"
17
+ },
18
+ "exports": {
19
+ "./package.json": "./package.json",
20
+ ".": {
21
+ "import": "./index.js",
22
+ "types": "./index.d.ts"
23
+ },
24
+ "./search.css": "./search.css"
25
+ },
26
+ "openPioneerFramework": {
27
+ "styles": "./search.css",
28
+ "services": [],
29
+ "i18n": {
30
+ "languages": [
31
+ "en",
32
+ "de"
33
+ ]
34
+ },
35
+ "ui": {
36
+ "references": []
37
+ },
38
+ "properties": [],
39
+ "packageFormatVersion": "1.0.0"
40
+ }
41
+ }
package/search.css ADDED
@@ -0,0 +1,29 @@
1
+ .search-component .chakra-divider {
2
+ display: none;
3
+ }
4
+
5
+ .search-component .search-highlighted-match {
6
+ font-weight: bold;
7
+ }
8
+
9
+ .search-component .search-value-container {
10
+ cursor: text;
11
+ padding-left: 30px !important;
12
+ }
13
+
14
+ .search-component .search-invisible {
15
+ display: none;
16
+ }
17
+
18
+ /* Exposition only
19
+
20
+ .search-option {
21
+ }
22
+
23
+ .search-no-match {
24
+ }
25
+
26
+ .search-loading-text {
27
+ } */
28
+
29
+ /*# sourceMappingURL=search.css.map */
package/search.css.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["search.css"],"names":[],"mappings":"AAAA;IACI,aAAa;AACjB;;AAEA;IACI,iBAAiB;AACrB;;AAEA;IACI,YAAY;IACZ,6BAA6B;AACjC;;AAEA;IACI,aAAa;AACjB;;AAEA;;;;;;;;;GASG","file":"search.css","sourcesContent":[".search-component .chakra-divider {\n display: none;\n}\n\n.search-component .search-highlighted-match {\n font-weight: bold;\n}\n\n.search-component .search-value-container {\n cursor: text;\n padding-left: 30px !important;\n}\n\n.search-component .search-invisible {\n display: none;\n}\n\n/* Exposition only\n\n.search-option {\n}\n\n.search-no-match {\n}\n\n.search-loading-text {\n} */\n"]}