@clyrex-digital/clyrex-controls-dev 0.8.1-dev.20260923102653 → 0.8.1-dev.20260923125229

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.
@@ -0,0 +1,377 @@
1
+ "use client";
2
+
3
+ "use client";
4
+ import {
5
+ InputControl_default
6
+ } from "./chunk-XDTWUEVR.mjs";
7
+ import {
8
+ Button_default
9
+ } from "./chunk-LSKD5U3S.mjs";
10
+ import {
11
+ InputControlType_default
12
+ } from "./chunk-F4DNLPFN.mjs";
13
+ import "./chunk-IMNQO57B.mjs";
14
+
15
+ // src/components/pageRenderingEngine/nodes/DataBindingControls.tsx
16
+ import { useCallback, useEffect, useRef, useState } from "react";
17
+ import { useRouter } from "next/navigation";
18
+ import { jsx, jsxs } from "react/jsx-runtime";
19
+ var ACTIVE_QUERY_CONTROLS_EVENT = "data-binding-active-query-controls";
20
+ var humanize = (value) => value.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
21
+ var escapeODataString = (value) => value.replace(/'/g, "''");
22
+ var parseFacetSelections = (filter) => {
23
+ const selections = /* @__PURE__ */ new Map();
24
+ const groupedExpression = /\(\s*([^()]+?)\s+eq\s+'((?:''|[^'])*)'\s*\)/gi;
25
+ for (const match of filter.matchAll(groupedExpression)) {
26
+ selections.set(match[1].trim(), match[2].replace(/''/g, "'"));
27
+ }
28
+ if (selections.size === 0) {
29
+ const ungroupedExpression = /(?:^|\s+and\s+)\s*([^()]+?)\s+eq\s+'((?:''|[^'])*)'\s*(?=\s+and\s+|$)/gi;
30
+ for (const match of filter.matchAll(ungroupedExpression)) {
31
+ selections.set(
32
+ match[1].trim(),
33
+ match[2].replace(/''/g, "'")
34
+ );
35
+ }
36
+ }
37
+ return selections;
38
+ };
39
+ var buildFacetFilter = (selections) => Array.from(selections.entries()).map(([field, value]) => `${field} eq '${escapeODataString(value)}'`).join(" and ");
40
+ var parseODataSearch = (value) => {
41
+ if (!value) return "";
42
+ const trimmedValue = value.trim();
43
+ if (trimmedValue.startsWith("'") && trimmedValue.endsWith("'")) {
44
+ return trimmedValue.slice(1, -1).replace(/''/g, "'");
45
+ }
46
+ return trimmedValue;
47
+ };
48
+ var SearchIcon = ({ className = "" }) => /* @__PURE__ */ jsxs(
49
+ "svg",
50
+ {
51
+ "aria-hidden": "true",
52
+ className,
53
+ fill: "none",
54
+ viewBox: "0 0 24 24",
55
+ stroke: "currentColor",
56
+ strokeWidth: "2",
57
+ children: [
58
+ /* @__PURE__ */ jsx("circle", { cx: "11", cy: "11", r: "7" }),
59
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", d: "m20 20-4-4" })
60
+ ]
61
+ }
62
+ );
63
+ var ResetIcon = ({ className = "" }) => /* @__PURE__ */ jsxs(
64
+ "svg",
65
+ {
66
+ "aria-hidden": "true",
67
+ className,
68
+ fill: "none",
69
+ viewBox: "0 0 24 24",
70
+ stroke: "currentColor",
71
+ strokeWidth: "2",
72
+ children: [
73
+ /* @__PURE__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M4 4v6h6" }),
74
+ /* @__PURE__ */ jsx(
75
+ "path",
76
+ {
77
+ strokeLinecap: "round",
78
+ strokeLinejoin: "round",
79
+ d: "M5.5 15a8 8 0 1 0 .6-7.8L4 10"
80
+ }
81
+ )
82
+ ]
83
+ }
84
+ );
85
+ var normalizeSortOption = (option) => {
86
+ const direction = option.direction.trim();
87
+ return {
88
+ label: option.title || humanize(option.name),
89
+ value: [option.name, direction].filter(Boolean).join(" ")
90
+ };
91
+ };
92
+ var DataBindingControls = ({
93
+ path,
94
+ query,
95
+ facets,
96
+ sortOptions,
97
+ showFacets,
98
+ showSortOptions,
99
+ showSearchBar,
100
+ mode = "toolbar"
101
+ }) => {
102
+ const router = useRouter();
103
+ const facetGroups = Object.entries(facets ?? {}).filter(
104
+ ([, values]) => Array.isArray(values) && values.length > 0
105
+ );
106
+ const normalizedSortOptions = (sortOptions ?? []).filter((option) => option?.name && option?.direction).map(normalizeSortOption).filter((option) => option.value);
107
+ const queryFilter = query?.["$filter"] ?? query?.filter ?? "";
108
+ const querySort = query?.["$orderby"] ?? query?.orderBy ?? "";
109
+ const [currentFilter, setCurrentFilter] = useState(queryFilter);
110
+ const [currentSort, setCurrentSort] = useState(querySort);
111
+ const selectedFacetValues = parseFacetSelections(currentFilter);
112
+ const currentSearch = parseODataSearch(query?.["$search"]);
113
+ const [searchText, setSearchText] = useState(currentSearch);
114
+ const lastNavigationUrl = useRef(null);
115
+ useEffect(() => {
116
+ setSearchText(currentSearch);
117
+ }, [currentSearch]);
118
+ useEffect(() => {
119
+ setCurrentFilter(queryFilter);
120
+ }, [queryFilter]);
121
+ useEffect(() => {
122
+ setCurrentSort(querySort);
123
+ }, [querySort]);
124
+ useEffect(() => {
125
+ const syncActiveQueryControls2 = (event) => {
126
+ const { filter, sort } = event.detail;
127
+ if (filter !== void 0) {
128
+ setCurrentFilter(filter);
129
+ }
130
+ if (sort !== void 0) {
131
+ setCurrentSort(sort);
132
+ }
133
+ };
134
+ window.addEventListener(
135
+ ACTIVE_QUERY_CONTROLS_EVENT,
136
+ syncActiveQueryControls2
137
+ );
138
+ return () => window.removeEventListener(
139
+ ACTIVE_QUERY_CONTROLS_EVENT,
140
+ syncActiveQueryControls2
141
+ );
142
+ }, []);
143
+ const syncActiveQueryControls = useCallback(
144
+ (updates) => {
145
+ window.dispatchEvent(
146
+ new CustomEvent(ACTIVE_QUERY_CONTROLS_EVENT, {
147
+ detail: updates
148
+ })
149
+ );
150
+ },
151
+ []
152
+ );
153
+ const getQueryUrl = useCallback(
154
+ (updates) => {
155
+ const params = new URLSearchParams(query ?? {});
156
+ Object.entries(updates).forEach(([key, value]) => {
157
+ if (value) {
158
+ params.set(key, value);
159
+ } else {
160
+ params.delete(key);
161
+ }
162
+ });
163
+ params.delete("$skip");
164
+ params.delete("skip");
165
+ const queryString = Array.from(params.entries()).map(
166
+ ([key, value]) => key === "$filter" || key === "filter" ? `${key}=${encodeURIComponent(value)}` : `${key}=${value}`
167
+ ).join("&");
168
+ return queryString ? `${path}?${queryString}` : path;
169
+ },
170
+ [path, query]
171
+ );
172
+ const getFacetUrl = (field, value) => {
173
+ const updatedSelections = new Map(selectedFacetValues);
174
+ if (value) {
175
+ updatedSelections.set(field, value);
176
+ } else {
177
+ updatedSelections.delete(field);
178
+ }
179
+ const updatedFilter = buildFacetFilter(updatedSelections);
180
+ syncActiveQueryControls({ filter: updatedFilter });
181
+ return getQueryUrl({
182
+ $filter: updatedFilter || void 0,
183
+ filter: void 0
184
+ });
185
+ };
186
+ const navigateWithoutReload = useCallback(
187
+ (url) => {
188
+ if (lastNavigationUrl.current === url) {
189
+ return;
190
+ }
191
+ if (typeof window !== "undefined") {
192
+ const targetUrl = new URL(url, window.location.origin);
193
+ if (targetUrl.pathname === window.location.pathname && targetUrl.search === window.location.search) {
194
+ return;
195
+ }
196
+ }
197
+ lastNavigationUrl.current = url;
198
+ router.push(url, { scroll: false });
199
+ },
200
+ [router]
201
+ );
202
+ const handleSearch = useCallback(async () => {
203
+ const trimmedSearchText = searchText.trim();
204
+ if (trimmedSearchText === currentSearch.trim()) {
205
+ return { isSuccessful: true };
206
+ }
207
+ navigateWithoutReload(
208
+ getQueryUrl({
209
+ "$search": trimmedSearchText || void 0
210
+ })
211
+ );
212
+ return { isSuccessful: true };
213
+ }, [currentSearch, getQueryUrl, navigateWithoutReload, searchText]);
214
+ const handleClearAll = useCallback(async () => {
215
+ syncActiveQueryControls({ filter: "", sort: "" });
216
+ navigateWithoutReload(
217
+ getQueryUrl({
218
+ $filter: void 0,
219
+ filter: void 0,
220
+ $orderby: void 0,
221
+ orderBy: void 0
222
+ })
223
+ );
224
+ return { isSuccessful: true };
225
+ }, [getQueryUrl, navigateWithoutReload, syncActiveQueryControls]);
226
+ const hasFacetControls = showFacets && facetGroups.length > 0;
227
+ const hasSortControls = showSortOptions && normalizedSortOptions.length > 0;
228
+ const hasActiveFiltersOrSort = Boolean(
229
+ currentFilter.trim() || currentSort.trim()
230
+ );
231
+ const shouldShowToolbarClearAll = hasActiveFiltersOrSort && !hasFacetControls;
232
+ const shouldShowToolbar = mode === "toolbar" && (showSearchBar || hasSortControls || shouldShowToolbarClearAll);
233
+ const shouldShowFacets = mode === "facets" && hasFacetControls;
234
+ if (!shouldShowToolbar && !shouldShowFacets) {
235
+ return null;
236
+ }
237
+ const renderFacetLinks = (field, values, stacked = false) => /* @__PURE__ */ jsxs("div", { className: stacked ? "flex flex-col gap-0.5" : "flex flex-wrap gap-1.5", children: [
238
+ /* @__PURE__ */ jsx(
239
+ "button",
240
+ {
241
+ type: "button",
242
+ onClick: () => navigateWithoutReload(getFacetUrl(field)),
243
+ "aria-pressed": !selectedFacetValues.has(field),
244
+ className: `cursor-pointer px-3 py-1.5 text-left text-sm transition-all duration-150 ease-out hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${stacked ? "w-full" : "border"} ${selectedFacetValues.has(field) ? "hover:bg-info-soft" : "bg-primary-base font-semibold hover:brightness-95"}`,
245
+ children: "All"
246
+ }
247
+ ),
248
+ values.map((facet) => {
249
+ const isSelected = selectedFacetValues.get(field) === facet.value;
250
+ return /* @__PURE__ */ jsxs(
251
+ "button",
252
+ {
253
+ type: "button",
254
+ onClick: () => navigateWithoutReload(getFacetUrl(field, facet.value)),
255
+ "aria-pressed": isSelected,
256
+ className: `cursor-pointer px-3 py-1.5 text-left text-sm transition-all duration-150 ease-out hover:shadow-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary ${stacked ? "w-full" : "border"} ${isSelected ? "bg-primary-base font-semibold hover:brightness-95" : "hover:bg-info-soft"}`,
257
+ children: [
258
+ facet.value,
259
+ " (",
260
+ facet.count,
261
+ ")"
262
+ ]
263
+ },
264
+ `${field}-${facet.value}`
265
+ );
266
+ })
267
+ ] });
268
+ return /* @__PURE__ */ jsxs(
269
+ "div",
270
+ {
271
+ className: mode === "toolbar" ? "my-6" : "flex flex-col gap-2 border bg-default rounded-[5px] p-3",
272
+ children: [
273
+ shouldShowToolbar && /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3 md:flex-row md:items-center md:justify-between", children: [
274
+ showSearchBar && /* @__PURE__ */ jsxs(
275
+ "form",
276
+ {
277
+ role: "search",
278
+ className: "flex w-full flex-col gap-3 rounded-xl border border-info bg-info-soft p-3 sm:flex-row sm:items-center md:min-w-0 md:flex-1",
279
+ onSubmit: (event) => {
280
+ event.preventDefault();
281
+ void handleSearch();
282
+ },
283
+ children: [
284
+ /* @__PURE__ */ jsxs("div", { className: "relative bg-default rounded-xl min-w-0 flex-1", children: [
285
+ /* @__PURE__ */ jsx(SearchIcon, { className: "pointer-events-none absolute left-4 top-1/2 z-10 h-5 w-5 -translate-y-1/2" }),
286
+ /* @__PURE__ */ jsx(
287
+ InputControl_default,
288
+ {
289
+ name: "searchText",
290
+ controlType: InputControlType_default.lineTextInput,
291
+ value: searchText,
292
+ callback: (updatedValues) => setSearchText(updatedValues.value),
293
+ attributes: {
294
+ placeholder: "Search discussions, topics, or keywords..."
295
+ },
296
+ inputClasses: "!mt-0 !rounded-xl !py-3 !pl-11 !pr-4 !shadow-sm"
297
+ }
298
+ )
299
+ ] }),
300
+ /* @__PURE__ */ jsxs(
301
+ Button_default,
302
+ {
303
+ ButtonType: "Primary" /* Solid */,
304
+ onClick: handleSearch,
305
+ className: "!rounded-xl !px-6 !py-3 sm:self-stretch",
306
+ children: [
307
+ /* @__PURE__ */ jsx(SearchIcon, { className: "h-5 w-5" }),
308
+ /* @__PURE__ */ jsx("span", { children: "Search" })
309
+ ]
310
+ }
311
+ )
312
+ ]
313
+ }
314
+ ),
315
+ hasSortControls && /* @__PURE__ */ jsxs("div", { className: "flex w-full items-center gap-3 rounded-2xl border border-info bg-info-soft p-3 text-sm md:ml-auto md:w-auto md:min-w-72 md:self-stretch", children: [
316
+ /* @__PURE__ */ jsx(
317
+ "label",
318
+ {
319
+ htmlFor: "sortOptions",
320
+ className: "shrink-0 whitespace-nowrap font-medium",
321
+ children: "Sort by"
322
+ }
323
+ ),
324
+ /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1 rounded-xl bg-default md:min-w-48", children: /* @__PURE__ */ jsx(
325
+ InputControl_default,
326
+ {
327
+ name: "sortOptions",
328
+ controlType: InputControlType_default.select,
329
+ value: currentSort,
330
+ dataset: normalizedSortOptions,
331
+ dataKeyFieldName: "value",
332
+ dataTextFieldName: "label",
333
+ callback: (updatedValues) => {
334
+ const updatedSort = updatedValues.value || "";
335
+ syncActiveQueryControls({ sort: updatedSort });
336
+ navigateWithoutReload(
337
+ getQueryUrl({
338
+ $orderby: updatedSort || void 0,
339
+ orderBy: void 0
340
+ })
341
+ );
342
+ },
343
+ attributes: { placeholder: "Default" },
344
+ inputClasses: "!mt-0 !rounded-xl !border-info !bg-default !px-4 !py-3 !text-body !shadow-sm focus:!border-primary focus:!ring-primary-base"
345
+ }
346
+ ) })
347
+ ] })
348
+ ] }),
349
+ shouldShowFacets && /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between gap-2 border-b pb-2", children: [
350
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: "Filters" }),
351
+ hasActiveFiltersOrSort && /* @__PURE__ */ jsxs(
352
+ "button",
353
+ {
354
+ type: "button",
355
+ onClick: () => void handleClearAll(),
356
+ className: "group inline-flex cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium text-primary transition-colors duration-150 hover:bg-info-soft hover:text-primary-800 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary",
357
+ "aria-label": "Clear all filters and sorting",
358
+ children: [
359
+ /* @__PURE__ */ jsx(ResetIcon, { className: "h-3.5 w-3.5 transition-transform duration-200 group-hover:-rotate-45" }),
360
+ /* @__PURE__ */ jsx("span", { children: "Clear all" })
361
+ ]
362
+ }
363
+ )
364
+ ] }),
365
+ shouldShowFacets && facetGroups.length === 1 && /* @__PURE__ */ jsx("nav", { "aria-label": `${humanize(facetGroups[0][0])} filters`, children: renderFacetLinks(facetGroups[0][0], facetGroups[0][1]) }),
366
+ shouldShowFacets && facetGroups.length > 1 && /* @__PURE__ */ jsx("div", { className: "flex flex-col", children: facetGroups.map(([field, values]) => /* @__PURE__ */ jsxs("details", { className: "border-b py-1 last:border-b-0", open: true, children: [
367
+ /* @__PURE__ */ jsx("summary", { className: "cursor-pointer rounded-md px-2 py-1.5 font-semibold transition-colors duration-150 hover:bg-info-soft focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary", children: humanize(field) }),
368
+ /* @__PURE__ */ jsx("div", { className: "px-1 pb-1 pt-0.5", children: renderFacetLinks(field, values, true) })
369
+ ] }, field)) })
370
+ ]
371
+ }
372
+ );
373
+ };
374
+ var DataBindingControls_default = DataBindingControls;
375
+ export {
376
+ DataBindingControls_default as default
377
+ };