Skip to content

Commit ba563eb

Browse files
committed
- Add searchResultsSearchField for quick finding on results pages
1 parent 45dedd1 commit ba563eb

File tree

3 files changed

+291
-0
lines changed

3 files changed

+291
-0
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<template>
2+
<GalleryAndResult :importCode="importCode"
3+
:exampleCode="exampleCode"
4+
:scriptSetupCode="scriptSetupCode"
5+
enableReflection>
6+
<div class="mb-3" style="display: flex;">
7+
<SearchResultsSearchField v-model="searchTerm"
8+
:searchResultsPageUrl="searchResultsPageUrl"
9+
:searchType="searchType"
10+
:placeholderText="placeholderText"
11+
:searchButtonIconCssClass="searchButtonIconCssClass"
12+
:additionalSearchParameters="additionalSearchParameters"
13+
:isButtonClickRequired="isButtonClickRequired" />
14+
</div>
15+
<div class="col-md-12" style="border: 1px solid #F27A1F; background-color: #f3f3f3;">
16+
<p class="font-italic">This section displays information not obvious from the control above. It will not show in actual use of the component and is for demonstration.</p>
17+
<dl>
18+
<dt>Search Results Page URL:</dt>
19+
<dd>{{ searchResultsPageUrl }}</dd>
20+
<dt>Search Type:</dt>
21+
<dd>{{ searchType }}</dd>
22+
<dt>Additional Search Parameters:</dt>
23+
<dd>{{ !!additionalSearchParameters ? JSON.stringify(additionalSearchParameters) : "<None Specified>" }}</dd>
24+
<dt>Is Button Click Required:</dt>
25+
<dd>{{ isButtonClickRequired }}</dd>
26+
<dt>URL That Will Be Navigated To:</dt>
27+
<dd>{{ computedFinalUrl }}</dd>
28+
</dl>
29+
</div>
30+
<template #settings>
31+
<div class="d-flex flex-row gap-3" style="max-height: 5vw;">
32+
<ContentSection title="Demonstratable Settings"
33+
icon="ti ti-settings"
34+
disableCollapse>
35+
<p class="text-semibold font-italic">The settings apply across many use-cases, so their usage and demonstration will show and occur if enabled or changed from defaults.</p>
36+
<ContentStack title="Toggle Properties"
37+
description="Properties that typically are either true or false.">
38+
<div class="d-flex flex-row gap-3">
39+
<Switch v-model="isButtonClickRequired"
40+
label="Button Click is Required"
41+
help="Determines if the search button needs to be clicked to perform the search or if pressing Enter in the text box will also perform the search." />
42+
</div>
43+
</ContentStack>
44+
<ContentStack title="Value Properties"
45+
description="Properties that typically require a value to be set.">
46+
<div class="d-flex flex-row gap-1">
47+
<div class="col-md-6">
48+
<p class="text-semibold">Field Display Values</p>
49+
<TextBox v-model="placeholderText"
50+
label="Placeholder Text"
51+
help="The placeholder text for the search's text box." />
52+
<TextBox v-model="searchButtonIconCssClass"
53+
label="Search Button Icon CSS Class"
54+
help="The CSS class for the icon displayed in the button that submits the search." />
55+
</div>
56+
<div class="col-md-6">
57+
<p class="text-semibold">Search Results</p>
58+
<TextBox v-model="searchResultsPageUrl"
59+
label="Search Results Page URL"
60+
help="The Search Results Page URL which will be navigated to and supplied query parameters. Ideally a NavigationUrlKey that is backend-validated, or raw URL." />
61+
<TextBox v-model="searchType"
62+
label="Search Type"
63+
help="The name of an exposed results field to look for the search term (modelValue) in once navigated to the search results page." />
64+
</div>
65+
</div>
66+
<div class="d-flex flex-row gap-1">
67+
<div class="col-md-6">
68+
<p class="text-semibold">Additional Search Parameters:</p>
69+
<KeyValueList v-model="additionalSearchParameters" keyPlaceholder="Key" valuePlaceholder="Value" />
70+
</div>
71+
</div>
72+
</ContentStack>
73+
</ContentSection>
74+
</div>
75+
</template>
76+
</GalleryAndResult>
77+
</template>
78+
79+
<script setup lang="ts">
80+
import { computed, ref } from "vue";
81+
import SearchResultsSearchField from "@Obsidian/Controls/searchResultsSearchField.obs";
82+
import GalleryAndResult from "./common/galleryAndResult.partial.obs";
83+
import ContentSection from "@Obsidian/Controls/contentSection.obs";
84+
import ContentStack from "@Obsidian/Controls/contentStack.obs";
85+
import TextBox from "@Obsidian/Controls/textBox.obs";
86+
import Switch from "@Obsidian/Controls/switch.obs";
87+
import KeyValueList from "@Obsidian/Controls/keyValueList.obs";
88+
import { KeyValueItem } from "@Obsidian/Types/Controls/keyValueItem";
89+
import { getSfcControlImportPath, ComponentUsage } from "./common/utils.partial";
90+
91+
const searchTerm = ref<string>("");
92+
const searchResultsPageUrl = ref<string>(new URL("/page/694", window.location.origin).toString());
93+
const searchType = ref<string>("name");
94+
const placeholderText = ref<string>("Quick Find");
95+
const searchButtonIconCssClass = ref<string>("ti ti-search");
96+
const additionalSearchParameters = ref<KeyValueItem[] | undefined>(undefined);
97+
const isButtonClickRequired = ref<boolean>(false);
98+
99+
const computedFinalUrl = computed<string>(() => {
100+
if (!searchTerm.value || searchTerm.value.trim() === "") {
101+
return "<No Search Term Entered>";
102+
}
103+
if (!searchResultsPageUrl.value || searchResultsPageUrl.value.trim() === "") {
104+
return "<No Search Results Page URL Entered>";
105+
}
106+
if (!searchType.value || searchType.value.trim() === "") {
107+
return "<No Search Type Entered>";
108+
}
109+
110+
const url = new URL(searchResultsPageUrl.value, window.location.origin);
111+
112+
if (searchType.value) {
113+
url.searchParams.append("searchType", searchType.value);
114+
}
115+
116+
url.searchParams.append("searchTerm", searchTerm.value);
117+
118+
if (additionalSearchParameters.value) {
119+
for (const param of additionalSearchParameters.value) {
120+
if (param.key && param.value) {
121+
url.searchParams.append(param.key, param.value);
122+
}
123+
}
124+
}
125+
126+
return url.toString();
127+
});
128+
129+
const importCode = getSfcControlImportPath("searchResultsSearchField");
130+
131+
const exampleCode = computed(() => {
132+
const usage = new ComponentUsage("SearchResultsSearchField");
133+
134+
usage.addAttribute(':modelValue="searchTerm"', true, false);
135+
usage.addAttribute(':searchResultsPageUrl="searchResultsPageUrl"', true, false);
136+
usage.addAttribute(':searchType="searchType"', searchType.value !== "name", false);
137+
usage.addAttribute(':placeholderText="placeholderText"', placeholderText.value !== "Quick Find", false);
138+
usage.addAttribute(':searchButtonIconCssClass="searchButtonIconCssClass"', searchButtonIconCssClass.value !== "ti ti-search", false);
139+
usage.addAttribute(':additionalSearchParameters="additionalSearchParameters"', additionalSearchParameters.value !== undefined, false);
140+
usage.addAttribute("isButtonClickRequired", isButtonClickRequired.value, false);
141+
142+
return usage.toString();
143+
});
144+
const scriptSetupCode = computed(() => {
145+
const usage = new ComponentUsage("SearchResultsSearchField");
146+
147+
usage.addScriptBody('const searchTerm = ref<string>("");');
148+
149+
usage.addScriptBodyWithComment(
150+
"Define the Search Results Page URL which will be navigated to and supplied query parameters. Ideally a NavigationUrlKey that is backend-validated",
151+
`const searchResultsPageUrl = ref<string>("${searchResultsPageUrl.value}");`
152+
);
153+
154+
if (searchType.value !== "name") {
155+
usage.addScriptBodyWithComment(
156+
"Define the name of an exposed results field to look for the search term in once navigated to the search results page.",
157+
`const searchType = ref<string>("${searchType.value}");`
158+
);
159+
}
160+
161+
if (placeholderText.value !== "Quick Find") {
162+
usage.addScriptBodyWithComment(
163+
"Define the placeholder text for the search text box.",
164+
`const placeholderText = ref<string>("${placeholderText.value}");`
165+
);
166+
}
167+
168+
if (searchButtonIconCssClass.value !== "ti ti-search") {
169+
usage.addScriptBodyWithComment(
170+
"Define the CSS class for the icon displayed in the search button.",
171+
`const searchButtonIconCssClass = ref<string>("${searchButtonIconCssClass.value}");`
172+
);
173+
}
174+
175+
if (additionalSearchParameters.value !== undefined) {
176+
const formattedSearchParameters = JSON.stringify(additionalSearchParameters.value, null, 8)
177+
.replace(/"\s+/g, '"') // Remove spaces after keys
178+
.replace(/\s+"/g, '"') // Remove spaces before values
179+
.replace(/^\s*/gm, " "); // Indent each line
180+
181+
usage.addScriptBodyWithComment(
182+
"Define any additional search parameters to include in the search results page URL.",
183+
`const additionalSearchParameters = ref<KeyValueItem[] | undefined>(\n${formattedSearchParameters}\n );`
184+
);
185+
}
186+
187+
return usage.toScriptSetupString();
188+
});
189+
</script>

Rock.JavaScript.Obsidian.Blocks/src/Example/controlGallery.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ import BarChartGallery from "./ControlGallery/barChartGallery.partial.obs";
259259
import PieChartGallery from "./ControlGallery/pieChartGallery.partial.obs";
260260
import ExperieceModePickerGallery from "./ControlGallery/experienceModePickerGallery.partial.obs";
261261
import PageTreeGallery from "./ControlGallery/pageTreeGallery.partial.obs";
262+
import SearchResultsSearchField from "./ControlGallery/searchResultsSearchFieldGallery.partial.obs";
262263

263264
const controlGalleryComponents: Record<string, Component> = [
264265
NotificationBoxGallery,
@@ -463,6 +464,7 @@ const controlGalleryComponents: Record<string, Component> = [
463464
PieChartGallery,
464465
ExperieceModePickerGallery,
465466
PageTreeGallery,
467+
SearchResultsSearchField,
466468
]
467469
// Fix vue 3 SFC putting name in __name.
468470
.map(a => {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<!-- Copyright by the Spark Development Network; Licensed under the Rock Community License -->
2+
<template>
3+
<TextBox v-model="internalValue"
4+
:placeholder="placeholderText"
5+
@keyup.enter="isButtonClickRequired ? null : onSubmitSearch()">
6+
<template #inputGroupAppend>
7+
<span class="input-group-btn">
8+
<button class="btn btn-default"
9+
type="button"
10+
@click.prevent="onSubmitSearch"
11+
title="">
12+
<i :class="searchButtonIconCssClass"></i>
13+
</button>
14+
</span>
15+
</template>
16+
</TextBox>
17+
</template>
18+
19+
<script setup lang="ts">
20+
import TextBox from "./textBox.obs";
21+
import { ListItemBag } from "@Obsidian/ViewModels/Utility/listItemBag";
22+
import { KeyValueItem } from "@Obsidian/Types/Controls/keyValueItem";
23+
import { useVModelPassthrough } from "@Obsidian/Utility/component";
24+
25+
const props = defineProps({
26+
/** The current search term of the search results search text box */
27+
modelValue: {
28+
type: String,
29+
required: true
30+
},
31+
/** The URL to the search results page. This can be supplied as a NavigationUrlKey for the raw URL */
32+
searchResultsPageUrl: {
33+
type: String,
34+
required: true
35+
},
36+
/** The name of and exposed results field to look for the search term in */
37+
searchType: {
38+
type: String,
39+
required: false,
40+
default: "name"
41+
},
42+
/** The placeholder text for the quick find text box */
43+
placeholderText: {
44+
type: String,
45+
required: false,
46+
default: "Quick Find"
47+
},
48+
/** The CSS class for the icon displayed in the button that submits the search */
49+
searchButtonIconCssClass: {
50+
type: String,
51+
required: false,
52+
default: "ti ti-search"
53+
},
54+
/** Additional search parameters to include in the search results page URL */
55+
additionalSearchParameters: {
56+
type: Array as () => KeyValueItem[] | undefined,
57+
required: false,
58+
default: undefined
59+
},
60+
/**
61+
* Determines if the search button needs to be clicked to perform the search or
62+
* if pressing Enter in the text box will also perform the search
63+
*/
64+
isButtonClickRequired: {
65+
type: Boolean,
66+
required: false,
67+
default: false
68+
}
69+
});
70+
71+
const emit = defineEmits<{
72+
(e: "update:modelValue", value: ListItemBag): void
73+
}>();
74+
75+
const internalValue = useVModelPassthrough(props, "modelValue", emit);
76+
77+
function onSubmitSearch(): void {
78+
if (!internalValue.value || internalValue.value.trim() === "") {
79+
return;
80+
}
81+
82+
const url = new URL(props.searchResultsPageUrl, window.location.origin);
83+
84+
if (props.searchType) {
85+
url.searchParams.set("SearchType", props.searchType);
86+
}
87+
88+
url.searchParams.set("SearchTerm", internalValue.value);
89+
90+
if (props.additionalSearchParameters) {
91+
for (const param of props.additionalSearchParameters) {
92+
if (param.key && param.value) {
93+
url.searchParams.set(param.key, param.value);
94+
}
95+
}
96+
}
97+
98+
window.location.href = url.toString();
99+
}
100+
</script>

0 commit comments

Comments
 (0)