@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 +16 -0
- package/CustomComponents.d.ts +11 -0
- package/CustomComponents.js +111 -0
- package/CustomComponents.js.map +1 -0
- package/LICENSE +202 -0
- package/README.md +109 -0
- package/Search.d.ts +63 -0
- package/Search.js +285 -0
- package/Search.js.map +1 -0
- package/SearchController.d.ts +21 -0
- package/SearchController.js +108 -0
- package/SearchController.js.map +1 -0
- package/_virtual/_virtual-pioneer-module_react-hooks.js +7 -0
- package/_virtual/_virtual-pioneer-module_react-hooks.js.map +1 -0
- package/api.d.ts +109 -0
- package/i18n/de.yaml +11 -0
- package/i18n/en.yaml +11 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/index.js.map +1 -0
- package/package.json +41 -0
- package/search.css +29 -0
- package/search.css.map +1 -0
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":"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"}
|
|
@@ -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
package/index.js
ADDED
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"]}
|