@planningcenter/topbar 12.3.0-qa-899.0 → 12.3.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/dist/topbar.css +26 -0
- package/dist/topbar.css.map +1 -1
- package/dist/topbar.js +872 -170
- package/dist/topbar.js.map +1 -1
- package/dist/topbar.modern.js +873 -171
- package/dist/topbar.modern.js.map +1 -1
- package/dist/topbar.module.js +873 -171
- package/dist/topbar.module.js.map +1 -1
- package/dist/topbar.umd.js +872 -170
- package/dist/topbar.umd.js.map +1 -1
- package/package.json +2 -3
package/dist/topbar.umd.js
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
var people__namespace = /*#__PURE__*/_interopNamespaceDefault(people);
|
|
28
28
|
var toolbar__namespace = /*#__PURE__*/_interopNamespaceDefault(toolbar);
|
|
29
29
|
|
|
30
|
-
var styles$
|
|
30
|
+
var styles$l = {"accessibility-toolbar":"accessibility-toolbar-jgjUG"};
|
|
31
31
|
|
|
32
32
|
const AccessibilityToolbar = ({
|
|
33
33
|
children
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
(_a = document.getElementById("main-content")) === null || _a === void 0 ? void 0 : _a.focus();
|
|
40
40
|
};
|
|
41
41
|
return /*#__PURE__*/React.createElement("div", {
|
|
42
|
-
className: styles$
|
|
42
|
+
className: styles$l["accessibility-toolbar"]
|
|
43
43
|
}, /*#__PURE__*/React.createElement(tapestry.Link, {
|
|
44
44
|
href: "#main-content",
|
|
45
45
|
kind: "primary",
|
|
@@ -356,11 +356,11 @@
|
|
|
356
356
|
fill: fill
|
|
357
357
|
})));
|
|
358
358
|
|
|
359
|
-
var styles$
|
|
359
|
+
var styles$k = {"announcement":"announcement-pI5tM","announcement--platform":"announcement--platform-EcUn2","announcement--promo":"announcement--promo-FarUk","announcement-icon":"announcement-icon-Ov39-","announcement-content":"announcement-content-q2ad6","dismiss":"dismiss-wUAoU","dismiss--promo":"dismiss--promo-7zvO4"};
|
|
360
360
|
|
|
361
361
|
function AnnouncementIcon() {
|
|
362
362
|
return /*#__PURE__*/React.createElement("svg", {
|
|
363
|
-
className: styles$
|
|
363
|
+
className: styles$k["announcement-icon"],
|
|
364
364
|
width: "16",
|
|
365
365
|
height: "16",
|
|
366
366
|
viewBox: "0 0 16 16",
|
|
@@ -485,7 +485,7 @@
|
|
|
485
485
|
} = this.props;
|
|
486
486
|
const isPromo = variant === "promo";
|
|
487
487
|
return /*#__PURE__*/React.createElement("div", {
|
|
488
|
-
className: classnames(styles$
|
|
488
|
+
className: classnames(styles$k["announcement"], styles$k[`announcement--${variant !== null && variant !== void 0 ? variant : "platform"}`]),
|
|
489
489
|
"data-announcement-variant": variant !== null && variant !== void 0 ? variant : "platform",
|
|
490
490
|
style: isPromo ? {
|
|
491
491
|
backgroundImage: promoBackgroundUrl,
|
|
@@ -494,7 +494,7 @@
|
|
|
494
494
|
backgroundSize: "cover"
|
|
495
495
|
} : undefined
|
|
496
496
|
}, !isPromo && /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
497
|
-
className: styles$
|
|
497
|
+
className: styles$k["announcement-content"],
|
|
498
498
|
style: style,
|
|
499
499
|
...platformProps
|
|
500
500
|
}));
|
|
@@ -543,7 +543,7 @@
|
|
|
543
543
|
const isPromo = variant === "promo";
|
|
544
544
|
return /*#__PURE__*/React.createElement("button", {
|
|
545
545
|
type: "button",
|
|
546
|
-
className: classnames(styles$
|
|
546
|
+
className: classnames(styles$k.dismiss, isPromo && styles$k["dismiss--promo"]),
|
|
547
547
|
...props
|
|
548
548
|
}, /*#__PURE__*/React.createElement(X, {
|
|
549
549
|
fill: isPromo ? "currentColor" : "white",
|
|
@@ -623,9 +623,9 @@
|
|
|
623
623
|
...platformProps
|
|
624
624
|
} = this.props;
|
|
625
625
|
return /*#__PURE__*/React.createElement("div", {
|
|
626
|
-
className: styles$
|
|
626
|
+
className: styles$k["announcement"]
|
|
627
627
|
}, /*#__PURE__*/React.createElement(AnnouncementIcon, null), /*#__PURE__*/React.createElement("div", {
|
|
628
|
-
className: styles$
|
|
628
|
+
className: styles$k["announcement-content"],
|
|
629
629
|
style: style,
|
|
630
630
|
...platformProps
|
|
631
631
|
}));
|
|
@@ -636,7 +636,7 @@
|
|
|
636
636
|
}) {
|
|
637
637
|
return /*#__PURE__*/React.createElement("button", {
|
|
638
638
|
type: "button",
|
|
639
|
-
className: styles$
|
|
639
|
+
className: styles$k.dismiss,
|
|
640
640
|
...props
|
|
641
641
|
}, /*#__PURE__*/React.createElement(X, {
|
|
642
642
|
fill: "white",
|
|
@@ -1758,7 +1758,7 @@
|
|
|
1758
1758
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported")));
|
|
1759
1759
|
}
|
|
1760
1760
|
|
|
1761
|
-
var styles$
|
|
1761
|
+
var styles$j = {"icon-slot":"icon-slot-m3ZIk","name":"name-UNqbW"};
|
|
1762
1762
|
|
|
1763
1763
|
const AppRow = ({
|
|
1764
1764
|
appName,
|
|
@@ -1767,24 +1767,24 @@
|
|
|
1767
1767
|
}) => {
|
|
1768
1768
|
if (appName === "Accounts") {
|
|
1769
1769
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("span", {
|
|
1770
|
-
className: styles$
|
|
1770
|
+
className: styles$j["icon-slot"]
|
|
1771
1771
|
}, /*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1772
1772
|
appName: "Accounts",
|
|
1773
1773
|
colors: colors,
|
|
1774
1774
|
size: iconSize
|
|
1775
1775
|
})), /*#__PURE__*/React.createElement("span", {
|
|
1776
|
-
className: styles$
|
|
1776
|
+
className: styles$j.name
|
|
1777
1777
|
}, "Account settings"));
|
|
1778
1778
|
}
|
|
1779
1779
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
1780
1780
|
appName: appName.replace(/[\s-]/, ""),
|
|
1781
1781
|
size: iconSize
|
|
1782
1782
|
}), /*#__PURE__*/React.createElement("span", {
|
|
1783
|
-
className: styles$
|
|
1783
|
+
className: styles$j.name
|
|
1784
1784
|
}, appName));
|
|
1785
1785
|
};
|
|
1786
1786
|
|
|
1787
|
-
var styles$
|
|
1787
|
+
var styles$i = {"button":"button-XMbeQ","label":"label-884of","chevron":"chevron-RRg9k"};
|
|
1788
1788
|
|
|
1789
1789
|
const AppsButton = /*#__PURE__*/React.forwardRef(({
|
|
1790
1790
|
appName,
|
|
@@ -1796,10 +1796,10 @@
|
|
|
1796
1796
|
...restProps,
|
|
1797
1797
|
ref: ref,
|
|
1798
1798
|
"aria-label": `App switcher: ${appName}`,
|
|
1799
|
-
className: styles$
|
|
1799
|
+
className: styles$i.button,
|
|
1800
1800
|
kind: "neutral-inline",
|
|
1801
1801
|
label: /*#__PURE__*/React.createElement("span", {
|
|
1802
|
-
className: styles$
|
|
1802
|
+
className: styles$i.label
|
|
1803
1803
|
}, isAccounts ? "Account settings" : appName),
|
|
1804
1804
|
prefix: isAccounts ? (/*#__PURE__*/React.createElement(MonoAppIcon, {
|
|
1805
1805
|
appName: "Accounts",
|
|
@@ -1810,13 +1810,13 @@
|
|
|
1810
1810
|
size: 22
|
|
1811
1811
|
})),
|
|
1812
1812
|
suffix: /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
1813
|
-
className: `${styles$
|
|
1813
|
+
className: `${styles$i.chevron} symbol`
|
|
1814
1814
|
})
|
|
1815
1815
|
});
|
|
1816
1816
|
});
|
|
1817
1817
|
AppsButton.displayName = "AppsButton";
|
|
1818
1818
|
|
|
1819
|
-
var styles$
|
|
1819
|
+
var styles$h = {"nav":"nav-y-6ML","popover-content":"popover-content-jOAAY","list":"list-jUxvp","link":"link-LMp2h","current":"current-4U4VL","accounts-item":"accounts-item-erJW0","accounts-link":"accounts-link-4jwPW"};
|
|
1820
1820
|
|
|
1821
1821
|
const AppsMenu = ({
|
|
1822
1822
|
appName,
|
|
@@ -1837,8 +1837,8 @@
|
|
|
1837
1837
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1838
1838
|
"aria-current": isCurrent ? "page" : undefined,
|
|
1839
1839
|
"aria-label": name,
|
|
1840
|
-
className: classnames(styles$
|
|
1841
|
-
[styles$
|
|
1840
|
+
className: classnames(styles$h.link, {
|
|
1841
|
+
[styles$h.current]: isCurrent
|
|
1842
1842
|
}),
|
|
1843
1843
|
"data-pendo": `app-switcher--change-app--${name}`,
|
|
1844
1844
|
"data-turbolinks": false,
|
|
@@ -1854,22 +1854,22 @@
|
|
|
1854
1854
|
colors: colors
|
|
1855
1855
|
})), /*#__PURE__*/React.createElement(tapestry.PopoverContent, {
|
|
1856
1856
|
"aria-label": "Switch app",
|
|
1857
|
-
className: styles$
|
|
1857
|
+
className: styles$h["popover-content"],
|
|
1858
1858
|
placement: "bottom start"
|
|
1859
1859
|
}, /*#__PURE__*/React.createElement("nav", {
|
|
1860
|
-
className: styles$
|
|
1860
|
+
className: styles$h.nav,
|
|
1861
1861
|
"data-pendo": "app-switcher"
|
|
1862
1862
|
}, /*#__PURE__*/React.createElement("ul", {
|
|
1863
|
-
className: styles$
|
|
1863
|
+
className: styles$h.list,
|
|
1864
1864
|
role: "list"
|
|
1865
1865
|
}, homeProduct && renderProductLink(homeProduct.name), restProducts.map(({
|
|
1866
1866
|
name
|
|
1867
1867
|
}) => renderProductLink(name)), accountsProduct && (/*#__PURE__*/React.createElement("li", {
|
|
1868
|
-
className: styles$
|
|
1868
|
+
className: styles$h["accounts-item"]
|
|
1869
1869
|
}, /*#__PURE__*/React.createElement("a", {
|
|
1870
1870
|
"aria-current": appName === accountsProduct.name ? "page" : undefined,
|
|
1871
1871
|
"aria-label": "Account settings",
|
|
1872
|
-
className: classnames(styles$
|
|
1872
|
+
className: classnames(styles$h.link, styles$h["accounts-link"]),
|
|
1873
1873
|
"data-pendo": "app-switcher--change-app--Accounts",
|
|
1874
1874
|
"data-turbolinks": false,
|
|
1875
1875
|
href: pcoUrl$1(env)("accounts") + getAppsSwitchProductPath(accountsProduct.name)
|
|
@@ -2077,7 +2077,7 @@
|
|
|
2077
2077
|
})) : (/*#__PURE__*/React.createElement("div", null, "appName not supported"));
|
|
2078
2078
|
}
|
|
2079
2079
|
|
|
2080
|
-
var styles$
|
|
2080
|
+
var styles$g = {"unbutton":"unbutton-lEbTR"};
|
|
2081
2081
|
|
|
2082
2082
|
function Unbutton({
|
|
2083
2083
|
className,
|
|
@@ -2086,7 +2086,7 @@
|
|
|
2086
2086
|
...nativeProps
|
|
2087
2087
|
}) {
|
|
2088
2088
|
return /*#__PURE__*/React.createElement("button", {
|
|
2089
|
-
className: classnames(styles$
|
|
2089
|
+
className: classnames(styles$g.unbutton, className),
|
|
2090
2090
|
style: {
|
|
2091
2091
|
...style
|
|
2092
2092
|
},
|
|
@@ -2095,7 +2095,7 @@
|
|
|
2095
2095
|
});
|
|
2096
2096
|
}
|
|
2097
2097
|
|
|
2098
|
-
var styles$
|
|
2098
|
+
var styles$f = {"button":"button-wsipc","button-accounts":"button-accounts-zQmBM","chevron":"chevron-obyBF","is-expanded":"is-expanded-PoCQ9"};
|
|
2099
2099
|
|
|
2100
2100
|
const LegacyAppsButton = ({
|
|
2101
2101
|
appName,
|
|
@@ -2110,8 +2110,8 @@
|
|
|
2110
2110
|
"aria-haspopup": "true",
|
|
2111
2111
|
"aria-label": `App switcher: ${appName}`,
|
|
2112
2112
|
onClick: onClick,
|
|
2113
|
-
className: classnames(styles$
|
|
2114
|
-
[styles$
|
|
2113
|
+
className: classnames(styles$f.button, {
|
|
2114
|
+
[styles$f["is-expanded"]]: expanded
|
|
2115
2115
|
}),
|
|
2116
2116
|
type: "button",
|
|
2117
2117
|
style: style,
|
|
@@ -2120,7 +2120,7 @@
|
|
|
2120
2120
|
appName: "Accounts",
|
|
2121
2121
|
colors: colors
|
|
2122
2122
|
}), /*#__PURE__*/React.createElement("div", {
|
|
2123
|
-
className: styles$
|
|
2123
|
+
className: styles$f["button-accounts"]
|
|
2124
2124
|
}, "Account settings"))) : (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(ColorAppIcon, {
|
|
2125
2125
|
appName: appName.replace(/[\s-]/, "")
|
|
2126
2126
|
}), /*#__PURE__*/React.createElement(MonoAppName, {
|
|
@@ -2128,7 +2128,7 @@
|
|
|
2128
2128
|
key: "mono-app-name",
|
|
2129
2129
|
size: 24
|
|
2130
2130
|
}))), /*#__PURE__*/React.createElement(DisclosureChevronIcon, {
|
|
2131
|
-
className: styles$
|
|
2131
|
+
className: styles$f.chevron,
|
|
2132
2132
|
key: "disclosure-chevron-icon"
|
|
2133
2133
|
}));
|
|
2134
2134
|
};
|
|
@@ -2261,7 +2261,7 @@
|
|
|
2261
2261
|
}
|
|
2262
2262
|
}
|
|
2263
2263
|
|
|
2264
|
-
var styles$
|
|
2264
|
+
var styles$e = {"popup-root":"popup-root-SeL5u","popup":"popup-nQXYW","is-visible":"is-visible-3DAUP"};
|
|
2265
2265
|
|
|
2266
2266
|
const Popup = ({
|
|
2267
2267
|
className,
|
|
@@ -2269,8 +2269,8 @@
|
|
|
2269
2269
|
isVisible,
|
|
2270
2270
|
...nativeProps
|
|
2271
2271
|
}) => (/*#__PURE__*/React.createElement(C, {
|
|
2272
|
-
className: classnames(styles$
|
|
2273
|
-
[styles$
|
|
2272
|
+
className: classnames(styles$e.popup, {
|
|
2273
|
+
[styles$e["is-visible"]]: isVisible
|
|
2274
2274
|
}, className),
|
|
2275
2275
|
...nativeProps
|
|
2276
2276
|
}));
|
|
@@ -2278,11 +2278,11 @@
|
|
|
2278
2278
|
const PopupRoot = ({
|
|
2279
2279
|
...nativeProps
|
|
2280
2280
|
}) => (/*#__PURE__*/React.createElement("div", {
|
|
2281
|
-
className: styles$
|
|
2281
|
+
className: styles$e["popup-root"],
|
|
2282
2282
|
...nativeProps
|
|
2283
2283
|
}));
|
|
2284
2284
|
|
|
2285
|
-
var styles$
|
|
2285
|
+
var styles$d = {"popup":"popup-bVhSa"};
|
|
2286
2286
|
|
|
2287
2287
|
const LegacyAppsMenu = ({
|
|
2288
2288
|
appName,
|
|
@@ -2320,7 +2320,7 @@
|
|
|
2320
2320
|
cleanup: !visible,
|
|
2321
2321
|
component: Outsider,
|
|
2322
2322
|
onOutsideClick: toggle,
|
|
2323
|
-
className: styles$
|
|
2323
|
+
className: styles$d.popup,
|
|
2324
2324
|
isVisible: visible
|
|
2325
2325
|
}, /*#__PURE__*/React.createElement("menu", {
|
|
2326
2326
|
ref: menuRef,
|
|
@@ -3230,7 +3230,97 @@ true;
|
|
|
3230
3230
|
});
|
|
3231
3231
|
}, []);
|
|
3232
3232
|
|
|
3233
|
+
const isMac = () => /Mac|iPhone|iPad|iPod/.test(navigator.platform);
|
|
3234
|
+
const hasCommandKey = event => isMac() ? event.metaKey : event.ctrlKey;
|
|
3235
|
+
const commandKeyLabel = () => isMac() ? "⌘" : "Ctrl+";
|
|
3236
|
+
|
|
3237
|
+
const STORAGE_KEY = "topbar.product_search.last_product";
|
|
3238
|
+
function readLastProduct() {
|
|
3239
|
+
try {
|
|
3240
|
+
return localStorage.getItem(STORAGE_KEY);
|
|
3241
|
+
} catch {
|
|
3242
|
+
return null;
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
function saveLastProduct(slug) {
|
|
3246
|
+
try {
|
|
3247
|
+
localStorage.setItem(STORAGE_KEY, slug);
|
|
3248
|
+
} catch {
|
|
3249
|
+
// Storage can be full or blocked; search still works without it.
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
var styles$c = {"visuallyHidden":"visuallyHidden-C28ju"};
|
|
3254
|
+
|
|
3255
|
+
var styles$b = {"tab":"tab-lQLDx"};
|
|
3256
|
+
|
|
3257
|
+
// useId isn't available on React 17, which our peer range still supports.
|
|
3258
|
+
let instanceCount$1 = 0;
|
|
3259
|
+
function useInstanceId(prefix) {
|
|
3260
|
+
const [id] = React.useState(() => `${prefix}-${++instanceCount$1}`);
|
|
3261
|
+
return id;
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3264
|
+
function ProductTabs({
|
|
3265
|
+
children,
|
|
3266
|
+
onKeyDown,
|
|
3267
|
+
onSelect,
|
|
3268
|
+
products,
|
|
3269
|
+
selected
|
|
3270
|
+
}) {
|
|
3271
|
+
const id = useInstanceId("product-tabs");
|
|
3272
|
+
const tabId = slug => `${id}-tab-${slug}`;
|
|
3273
|
+
const tabRefs = React.useRef(new Map());
|
|
3274
|
+
const handleKeyDown = event => {
|
|
3275
|
+
var _a;
|
|
3276
|
+
const offset = {
|
|
3277
|
+
ArrowLeft: -1,
|
|
3278
|
+
ArrowRight: 1
|
|
3279
|
+
}[event.key];
|
|
3280
|
+
if (offset) {
|
|
3281
|
+
event.preventDefault();
|
|
3282
|
+
const index = products.indexOf(selected);
|
|
3283
|
+
const next = products[(index + offset + products.length) % products.length];
|
|
3284
|
+
onSelect(next.slug);
|
|
3285
|
+
(_a = tabRefs.current.get(next.slug)) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3286
|
+
} else {
|
|
3287
|
+
onKeyDown === null || onKeyDown === void 0 ? void 0 : onKeyDown(event);
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("div", {
|
|
3291
|
+
"aria-label": "Product",
|
|
3292
|
+
onKeyDown: handleKeyDown,
|
|
3293
|
+
role: "tablist"
|
|
3294
|
+
}, products.map(product => (/*#__PURE__*/React.createElement("button", {
|
|
3295
|
+
"aria-controls": `${id}-panel`,
|
|
3296
|
+
"aria-selected": product === selected,
|
|
3297
|
+
className: styles$b.tab,
|
|
3298
|
+
id: tabId(product.slug),
|
|
3299
|
+
key: product.slug,
|
|
3300
|
+
onClick: () => onSelect(product.slug),
|
|
3301
|
+
ref: element => {
|
|
3302
|
+
if (element) {
|
|
3303
|
+
tabRefs.current.set(product.slug, element);
|
|
3304
|
+
} else {
|
|
3305
|
+
tabRefs.current.delete(product.slug);
|
|
3306
|
+
}
|
|
3307
|
+
},
|
|
3308
|
+
role: "tab",
|
|
3309
|
+
tabIndex: product === selected ? 0 : -1,
|
|
3310
|
+
type: "button"
|
|
3311
|
+
}, product.appName)))), /*#__PURE__*/React.createElement("div", {
|
|
3312
|
+
"aria-labelledby": tabId(selected.slug),
|
|
3313
|
+
id: `${id}-panel`,
|
|
3314
|
+
role: "tabpanel"
|
|
3315
|
+
}, children));
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
var styles$a = {"result":"result-WCnxr"};
|
|
3319
|
+
|
|
3233
3320
|
function SearchResult({
|
|
3321
|
+
highlighted,
|
|
3322
|
+
id,
|
|
3323
|
+
onHighlight,
|
|
3234
3324
|
result
|
|
3235
3325
|
}) {
|
|
3236
3326
|
const {
|
|
@@ -3238,62 +3328,222 @@ true;
|
|
|
3238
3328
|
title,
|
|
3239
3329
|
url
|
|
3240
3330
|
} = result.attributes;
|
|
3331
|
+
const ref = React.useRef(null);
|
|
3332
|
+
React.useEffect(() => {
|
|
3333
|
+
var _a;
|
|
3334
|
+
if (highlighted) (_a = ref.current) === null || _a === void 0 ? void 0 : _a.scrollIntoView({
|
|
3335
|
+
block: "nearest"
|
|
3336
|
+
});
|
|
3337
|
+
}, [highlighted]);
|
|
3338
|
+
// Focus stays in the search input, which points at the highlighted result
|
|
3339
|
+
// with aria-activedescendant, so results are kept out of the tab order.
|
|
3340
|
+
// Mouse move rather than enter: arrowing scrolls results under a resting
|
|
3341
|
+
// pointer, and that shouldn't steal the highlight.
|
|
3241
3342
|
return /*#__PURE__*/React.createElement("a", {
|
|
3242
|
-
|
|
3343
|
+
"aria-selected": highlighted,
|
|
3344
|
+
className: styles$a.result,
|
|
3345
|
+
href: url,
|
|
3346
|
+
id: id,
|
|
3347
|
+
onMouseMove: onHighlight,
|
|
3348
|
+
ref: ref,
|
|
3349
|
+
role: "option",
|
|
3350
|
+
tabIndex: -1
|
|
3243
3351
|
}, /*#__PURE__*/React.createElement("div", null, title), /*#__PURE__*/React.createElement("div", null, summarize(metadata)));
|
|
3244
3352
|
}
|
|
3245
3353
|
function summarize(metadata) {
|
|
3246
3354
|
return Object.values(metadata !== null && metadata !== void 0 ? metadata : {}).filter(value => typeof value === "string" && value !== "").join(" · ");
|
|
3247
3355
|
}
|
|
3248
3356
|
|
|
3249
|
-
const
|
|
3250
|
-
function
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3357
|
+
const resultId = (listboxId, index) => `${listboxId}-result-${index}`;
|
|
3358
|
+
function SearchResults({
|
|
3359
|
+
search,
|
|
3360
|
+
term,
|
|
3361
|
+
...navigation
|
|
3362
|
+
}) {
|
|
3363
|
+
var _a;
|
|
3364
|
+
switch (search.status) {
|
|
3365
|
+
case "idle":
|
|
3366
|
+
return null;
|
|
3367
|
+
case "loading":
|
|
3368
|
+
if ((_a = search.groups) === null || _a === void 0 ? void 0 : _a.length) {
|
|
3369
|
+
return /*#__PURE__*/React.createElement(ResultGroups, {
|
|
3370
|
+
busy: true,
|
|
3371
|
+
groups: search.groups,
|
|
3372
|
+
...navigation
|
|
3373
|
+
});
|
|
3374
|
+
}
|
|
3375
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
3376
|
+
role: "status"
|
|
3377
|
+
}, "Searching\u2026");
|
|
3378
|
+
case "error":
|
|
3379
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
3380
|
+
role: "alert"
|
|
3381
|
+
}, "Search isn\u2019t working right now. Try again.");
|
|
3382
|
+
case "done":
|
|
3383
|
+
if (search.groups.length === 0) {
|
|
3384
|
+
return /*#__PURE__*/React.createElement("p", {
|
|
3385
|
+
role: "status"
|
|
3386
|
+
}, "No results for \u201C", term.trim(), "\u201D");
|
|
3387
|
+
}
|
|
3388
|
+
return /*#__PURE__*/React.createElement(ResultGroups, {
|
|
3389
|
+
groups: search.groups,
|
|
3390
|
+
...navigation
|
|
3391
|
+
});
|
|
3392
|
+
}
|
|
3393
|
+
}
|
|
3394
|
+
function ResultGroups({
|
|
3395
|
+
busy = false,
|
|
3396
|
+
groups,
|
|
3397
|
+
highlightedIndex,
|
|
3398
|
+
id,
|
|
3399
|
+
label,
|
|
3400
|
+
onHighlight
|
|
3401
|
+
}) {
|
|
3402
|
+
let index = 0;
|
|
3403
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
3404
|
+
"aria-busy": busy,
|
|
3405
|
+
"aria-label": label,
|
|
3406
|
+
id: id,
|
|
3407
|
+
role: "listbox"
|
|
3408
|
+
}, groups.map(({
|
|
3409
|
+
results,
|
|
3410
|
+
type
|
|
3411
|
+
}, groupIndex) => {
|
|
3412
|
+
const headingId = `${id}-group-${groupIndex}`;
|
|
3413
|
+
return /*#__PURE__*/React.createElement("div", {
|
|
3414
|
+
"aria-labelledby": headingId,
|
|
3415
|
+
key: type,
|
|
3416
|
+
role: "group"
|
|
3417
|
+
}, /*#__PURE__*/React.createElement("div", {
|
|
3418
|
+
id: headingId
|
|
3419
|
+
}, type), results.map(result => {
|
|
3420
|
+
const resultIndex = index++;
|
|
3421
|
+
return /*#__PURE__*/React.createElement(SearchResult, {
|
|
3422
|
+
highlighted: resultIndex === highlightedIndex,
|
|
3423
|
+
id: resultId(id, resultIndex),
|
|
3424
|
+
key: result.id,
|
|
3425
|
+
onHighlight: () => onHighlight(resultIndex),
|
|
3426
|
+
result: result
|
|
3427
|
+
});
|
|
3428
|
+
}));
|
|
3429
|
+
}));
|
|
3430
|
+
}
|
|
3431
|
+
|
|
3432
|
+
const CACHE_TTL_MS = 60000;
|
|
3433
|
+
const MAX_ENTRIES$1 = 50;
|
|
3434
|
+
const cache = new Map();
|
|
3435
|
+
function cachedRequest(url) {
|
|
3436
|
+
const cached = cache.get(url);
|
|
3437
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
3438
|
+
// Re-inserting keeps the Map in recently-used order for eviction.
|
|
3439
|
+
cache.delete(url);
|
|
3440
|
+
cache.set(url, cached);
|
|
3441
|
+
return cached.promise;
|
|
3442
|
+
} else {
|
|
3443
|
+
return fetchIntoCache(url);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
function fetchIntoCache(url) {
|
|
3447
|
+
const promise = apiRequest(defaultFetch, url).then(({
|
|
3448
|
+
json,
|
|
3449
|
+
ok
|
|
3450
|
+
}) => {
|
|
3451
|
+
if (!ok || json === null) throw new Error(`Request failed: ${url}`);
|
|
3452
|
+
return json;
|
|
3255
3453
|
});
|
|
3454
|
+
// In-flight entries never expire, so every caller shares the one fetch.
|
|
3455
|
+
const entry = {
|
|
3456
|
+
expiresAt: Infinity,
|
|
3457
|
+
promise
|
|
3458
|
+
};
|
|
3459
|
+
cache.set(url, entry);
|
|
3460
|
+
promise.then(() => entry.expiresAt = Date.now() + CACHE_TTL_MS, () => cache.get(url) === entry && cache.delete(url));
|
|
3461
|
+
if (cache.size > MAX_ENTRIES$1) cache.delete(cache.keys().next().value);
|
|
3462
|
+
return promise;
|
|
3463
|
+
}
|
|
3464
|
+
function useCachedRequest(url) {
|
|
3465
|
+
const [settled, setSettled] = React.useState();
|
|
3256
3466
|
React.useEffect(() => {
|
|
3257
|
-
if (
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3467
|
+
if (url) {
|
|
3468
|
+
let current = true;
|
|
3469
|
+
cachedRequest(url).then(data => current && setSettled({
|
|
3470
|
+
data,
|
|
3471
|
+
url
|
|
3472
|
+
}), () => current && setSettled({
|
|
3473
|
+
failed: true,
|
|
3474
|
+
url
|
|
3475
|
+
}));
|
|
3476
|
+
return () => {
|
|
3477
|
+
current = false;
|
|
3478
|
+
};
|
|
3479
|
+
} else {
|
|
3261
3480
|
return undefined;
|
|
3262
3481
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3482
|
+
}, [url]);
|
|
3483
|
+
if (!url) {
|
|
3484
|
+
return {
|
|
3485
|
+
status: "idle"
|
|
3486
|
+
};
|
|
3487
|
+
} else if ((settled === null || settled === void 0 ? void 0 : settled.url) !== url) {
|
|
3488
|
+
return {
|
|
3489
|
+
data: settled && "data" in settled ? settled.data : undefined,
|
|
3265
3490
|
status: "loading"
|
|
3266
|
-
});
|
|
3267
|
-
apiRequest(defaultFetch, productSearchUrl(debouncedQuery)).then(({
|
|
3268
|
-
json,
|
|
3269
|
-
ok
|
|
3270
|
-
}) => {
|
|
3271
|
-
if (!current) return;
|
|
3272
|
-
setSearch(ok && json ? {
|
|
3273
|
-
groups: groupByType(json),
|
|
3274
|
-
status: "done"
|
|
3275
|
-
} : {
|
|
3276
|
-
status: "error"
|
|
3277
|
-
});
|
|
3278
|
-
});
|
|
3279
|
-
return () => {
|
|
3280
|
-
current = false;
|
|
3281
3491
|
};
|
|
3282
|
-
}
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3492
|
+
} else if ("data" in settled) {
|
|
3493
|
+
return {
|
|
3494
|
+
data: settled.data,
|
|
3495
|
+
status: "done"
|
|
3496
|
+
};
|
|
3497
|
+
} else {
|
|
3498
|
+
return {
|
|
3499
|
+
status: "error"
|
|
3500
|
+
};
|
|
3501
|
+
}
|
|
3502
|
+
}
|
|
3503
|
+
|
|
3504
|
+
const SEARCH_DEBOUNCE_MS = 300;
|
|
3505
|
+
function useProductSearch(product, term) {
|
|
3506
|
+
const query = term.trim();
|
|
3507
|
+
const debouncedQuery = useDebouncedValue(query, SEARCH_DEBOUNCE_MS);
|
|
3508
|
+
const request = useCachedRequest(debouncedQuery ? productSearchUrl(product, debouncedQuery) : null);
|
|
3509
|
+
const response = "data" in request ? request.data : undefined;
|
|
3510
|
+
const settledProduct = useSettledProduct(product, request.status);
|
|
3511
|
+
// While a request loads, the previous response stays on screen, but only
|
|
3512
|
+
// if it was for this product: People results must never show under the
|
|
3513
|
+
// Services tab.
|
|
3514
|
+
const visibleResponse = request.status === "done" || settledProduct === product ? response : undefined;
|
|
3515
|
+
const groups = React.useMemo(() => visibleResponse && groupByType(visibleResponse), [visibleResponse]);
|
|
3516
|
+
if (!query) {
|
|
3517
|
+
return {
|
|
3518
|
+
status: "idle"
|
|
3519
|
+
};
|
|
3520
|
+
} else if (query !== debouncedQuery) {
|
|
3521
|
+
return {
|
|
3522
|
+
groups,
|
|
3523
|
+
status: "loading"
|
|
3524
|
+
};
|
|
3525
|
+
} else if (request.status === "done") {
|
|
3526
|
+
return {
|
|
3527
|
+
groups: groups !== null && groups !== void 0 ? groups : [],
|
|
3528
|
+
status: "done"
|
|
3529
|
+
};
|
|
3530
|
+
} else if (request.status === "error") {
|
|
3531
|
+
return {
|
|
3532
|
+
status: "error"
|
|
3533
|
+
};
|
|
3534
|
+
} else {
|
|
3535
|
+
return {
|
|
3536
|
+
groups,
|
|
3537
|
+
status: "loading"
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3290
3540
|
}
|
|
3291
|
-
function productSearchUrl(query) {
|
|
3541
|
+
function productSearchUrl(product, query) {
|
|
3292
3542
|
const params = new URLSearchParams({
|
|
3293
3543
|
per_page: "100",
|
|
3294
3544
|
"where[term]": query
|
|
3295
3545
|
});
|
|
3296
|
-
return `${window.location.origin}/~api/
|
|
3546
|
+
return `${window.location.origin}/~api/${product}/v2/product_search_results?${params}`;
|
|
3297
3547
|
}
|
|
3298
3548
|
function groupByType({
|
|
3299
3549
|
data,
|
|
@@ -3318,6 +3568,14 @@ true;
|
|
|
3318
3568
|
};
|
|
3319
3569
|
return [...groups.values()].sort((a, b) => rank(a.type) - rank(b.type));
|
|
3320
3570
|
}
|
|
3571
|
+
// The product the most recent finished request was for.
|
|
3572
|
+
function useSettledProduct(product, status) {
|
|
3573
|
+
const [settledProduct, setSettledProduct] = React.useState();
|
|
3574
|
+
React.useEffect(() => {
|
|
3575
|
+
if (status === "done") setSettledProduct(product);
|
|
3576
|
+
}, [product, status]);
|
|
3577
|
+
return settledProduct;
|
|
3578
|
+
}
|
|
3321
3579
|
function useDebouncedValue(value, delay) {
|
|
3322
3580
|
const [debounced, setDebounced] = React.useState(value);
|
|
3323
3581
|
React.useEffect(() => {
|
|
@@ -3327,53 +3585,199 @@ true;
|
|
|
3327
3585
|
return debounced;
|
|
3328
3586
|
}
|
|
3329
3587
|
|
|
3330
|
-
|
|
3331
|
-
|
|
3588
|
+
const COPIED_FEEDBACK_MS = 1500;
|
|
3589
|
+
const verticalStep = key => ({
|
|
3590
|
+
ArrowDown: 1,
|
|
3591
|
+
ArrowUp: -1
|
|
3592
|
+
})[key];
|
|
3593
|
+
function useResultNavigation(results) {
|
|
3594
|
+
// Remembering which results the index belongs to puts the highlight back on
|
|
3595
|
+
// the first result whenever new ones arrive, without an effect to reset it.
|
|
3596
|
+
const [highlight, setHighlight] = React.useState({
|
|
3597
|
+
index: 0,
|
|
3598
|
+
results
|
|
3599
|
+
});
|
|
3600
|
+
const highlightedIndex = highlight.results === results ? highlight.index : 0;
|
|
3601
|
+
const highlighted = results[highlightedIndex];
|
|
3602
|
+
const [copiedUrl, setCopiedUrl] = React.useState(null);
|
|
3603
|
+
React.useEffect(() => {
|
|
3604
|
+
if (copiedUrl) {
|
|
3605
|
+
const timeout = setTimeout(() => setCopiedUrl(null), COPIED_FEEDBACK_MS);
|
|
3606
|
+
return () => clearTimeout(timeout);
|
|
3607
|
+
} else {
|
|
3608
|
+
return undefined;
|
|
3609
|
+
}
|
|
3610
|
+
}, [copiedUrl]);
|
|
3611
|
+
const highlightIndex = React.useCallback(index => setHighlight(current => current.results === results && current.index === index ? current : {
|
|
3612
|
+
index,
|
|
3613
|
+
results
|
|
3614
|
+
}), [results]);
|
|
3615
|
+
const handleKeyDown = event => {
|
|
3616
|
+
const step = verticalStep(event.key);
|
|
3617
|
+
if (step && results.length > 0) {
|
|
3618
|
+
event.preventDefault();
|
|
3619
|
+
highlightIndex((highlightedIndex + step + results.length) % results.length);
|
|
3620
|
+
} else if (event.key === "Enter" && highlighted && !event.nativeEvent.isComposing) {
|
|
3621
|
+
event.preventDefault();
|
|
3622
|
+
open(highlighted.attributes.url, {
|
|
3623
|
+
newTab: hasCommandKey(event)
|
|
3624
|
+
});
|
|
3625
|
+
} else if (isCopyShortcut(event) && highlighted && !hasSelectedText(event.currentTarget)) {
|
|
3626
|
+
event.preventDefault();
|
|
3627
|
+
const {
|
|
3628
|
+
url
|
|
3629
|
+
} = highlighted.attributes;
|
|
3630
|
+
navigator.clipboard.writeText(url).then(() => setCopiedUrl(url));
|
|
3631
|
+
}
|
|
3632
|
+
};
|
|
3633
|
+
return {
|
|
3634
|
+
copied: copiedUrl !== null && copiedUrl === (highlighted === null || highlighted === void 0 ? void 0 : highlighted.attributes.url),
|
|
3635
|
+
handleKeyDown,
|
|
3636
|
+
highlightIndex,
|
|
3637
|
+
highlighted,
|
|
3638
|
+
highlightedIndex: highlighted ? highlightedIndex : null
|
|
3639
|
+
};
|
|
3640
|
+
}
|
|
3641
|
+
function open(url, {
|
|
3642
|
+
newTab
|
|
3332
3643
|
}) {
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3644
|
+
if (newTab) {
|
|
3645
|
+
window.open(url, "_blank", "noopener");
|
|
3646
|
+
} else {
|
|
3647
|
+
window.location.assign(url);
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
const isCopyShortcut = event => event.key.toLowerCase() === "c" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3651
|
+
// With text selected, ⌘C should copy that text as usual, not the link.
|
|
3652
|
+
function hasSelectedText(element) {
|
|
3653
|
+
var _a, _b;
|
|
3654
|
+
const selectedInField = element instanceof HTMLInputElement && element.selectionStart !== element.selectionEnd;
|
|
3655
|
+
return selectedInField || ((_b = (_a = window.getSelection()) === null || _a === void 0 ? void 0 : _a.toString()) !== null && _b !== void 0 ? _b : "") !== "";
|
|
3656
|
+
}
|
|
3657
|
+
|
|
3658
|
+
const SEARCHABLE_PRODUCTS = [{
|
|
3659
|
+
appName: "Groups",
|
|
3660
|
+
slug: "groups"
|
|
3661
|
+
}, {
|
|
3662
|
+
appName: "People",
|
|
3663
|
+
slug: "people"
|
|
3664
|
+
}, {
|
|
3665
|
+
appName: "Services",
|
|
3666
|
+
slug: "services"
|
|
3667
|
+
}];
|
|
3668
|
+
const PERMISSION_CHECKS = {
|
|
3669
|
+
people: () => fetchMe("people", "people_permissions,site_administrator").then(({
|
|
3670
|
+
people_permissions,
|
|
3671
|
+
site_administrator
|
|
3672
|
+
}) => site_administrator || people_permissions !== null),
|
|
3673
|
+
services: () => fetchMe("services", "can_search").then(({
|
|
3674
|
+
can_search
|
|
3675
|
+
}) => can_search)
|
|
3676
|
+
};
|
|
3677
|
+
function fetchMe(product, fields) {
|
|
3678
|
+
const params = new URLSearchParams({
|
|
3679
|
+
"fields[Person]": fields
|
|
3680
|
+
});
|
|
3681
|
+
return apiRequest(defaultFetch, `${window.location.origin}/~api/${product}/v2/me?${params}`).then(({
|
|
3682
|
+
json,
|
|
3683
|
+
ok
|
|
3684
|
+
}) => {
|
|
3685
|
+
if (!ok || json === null) throw new Error("Permission check failed");
|
|
3686
|
+
return json.data.attributes;
|
|
3687
|
+
});
|
|
3688
|
+
}
|
|
3689
|
+
// Permissions rarely change mid-session, so a successful check is kept until
|
|
3690
|
+
// the page reloads. A failed one is dropped so a remount tries again.
|
|
3691
|
+
const permissionCache = new Map();
|
|
3692
|
+
function canSearch(slug) {
|
|
3693
|
+
const check = PERMISSION_CHECKS[slug];
|
|
3694
|
+
const cached = permissionCache.get(slug);
|
|
3695
|
+
if (!check) {
|
|
3696
|
+
return Promise.resolve(true);
|
|
3697
|
+
} else if (cached) {
|
|
3698
|
+
return cached;
|
|
3699
|
+
} else {
|
|
3700
|
+
const promise = check().catch(() => {
|
|
3701
|
+
permissionCache.delete(slug);
|
|
3702
|
+
return false;
|
|
3703
|
+
});
|
|
3704
|
+
permissionCache.set(slug, promise);
|
|
3705
|
+
return promise;
|
|
3706
|
+
}
|
|
3707
|
+
}
|
|
3708
|
+
function useSearchableProducts(apps) {
|
|
3709
|
+
// Keyed by name because providers hand over a fresh `apps` array on renders
|
|
3710
|
+
// where nothing changed.
|
|
3711
|
+
const accessibleKey = inSwitcherOrder(apps).map(({
|
|
3712
|
+
slug
|
|
3713
|
+
}) => slug).join(",");
|
|
3714
|
+
const [settled, setSettled] = React.useState();
|
|
3715
|
+
React.useEffect(() => {
|
|
3716
|
+
let current = true;
|
|
3717
|
+
const accessible = accessibleKey.split(",").flatMap(slug => SEARCHABLE_PRODUCTS.filter(product => product.slug === slug));
|
|
3718
|
+
Promise.all(accessible.map(({
|
|
3719
|
+
slug
|
|
3720
|
+
}) => canSearch(slug))).then(allowed => {
|
|
3721
|
+
if (current) {
|
|
3722
|
+
setSettled({
|
|
3723
|
+
accessibleKey,
|
|
3724
|
+
products: accessible.filter((_, index) => allowed[index])
|
|
3725
|
+
});
|
|
3350
3726
|
}
|
|
3351
|
-
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3727
|
+
});
|
|
3728
|
+
return () => {
|
|
3729
|
+
current = false;
|
|
3730
|
+
};
|
|
3731
|
+
}, [accessibleKey]);
|
|
3732
|
+
if ((settled === null || settled === void 0 ? void 0 : settled.accessibleKey) === accessibleKey) {
|
|
3733
|
+
return {
|
|
3734
|
+
products: settled.products,
|
|
3735
|
+
status: "done"
|
|
3736
|
+
};
|
|
3737
|
+
} else {
|
|
3738
|
+
return {
|
|
3739
|
+
status: "loading"
|
|
3740
|
+
};
|
|
3362
3741
|
}
|
|
3363
3742
|
}
|
|
3743
|
+
function inSwitcherOrder(apps) {
|
|
3744
|
+
return groupApps(apps).restProducts.flatMap(app => SEARCHABLE_PRODUCTS.filter(product => product.appName === app.name));
|
|
3745
|
+
}
|
|
3364
3746
|
|
|
3365
|
-
const
|
|
3366
|
-
|
|
3367
|
-
|
|
3747
|
+
const isSearchShortcut = event => event.key.toLowerCase() === "k" && !event.altKey && !event.shiftKey && hasCommandKey(event);
|
|
3748
|
+
// Products opened their own search with `/` before this one replaced it.
|
|
3749
|
+
const isLegacySearchShortcut = event => event.key === "/" && !event.altKey && !event.ctrlKey && !event.metaKey && !isTypingIn(event.target);
|
|
3750
|
+
const isTypingKey = event => event.key.length === 1 && event.key !== " " && !event.altKey && !event.ctrlKey && !event.metaKey;
|
|
3751
|
+
const isTypingIn = target => target instanceof Element && target.closest('input, select, textarea, [contenteditable]:not([contenteditable="false"])') !== null;
|
|
3752
|
+
function ProductSearch({
|
|
3753
|
+
apps
|
|
3754
|
+
}) {
|
|
3755
|
+
const searchable = useSearchableProducts(apps);
|
|
3756
|
+
if (searchable.status === "done" && searchable.products.length > 0) {
|
|
3757
|
+
return /*#__PURE__*/React.createElement(SearchButtonAndDialog, {
|
|
3758
|
+
products: searchable.products
|
|
3759
|
+
});
|
|
3760
|
+
} else {
|
|
3761
|
+
return null;
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
function SearchButtonAndDialog({
|
|
3765
|
+
products
|
|
3766
|
+
}) {
|
|
3368
3767
|
const [open, setOpen] = React.useState(false);
|
|
3369
3768
|
React.useEffect(() => {
|
|
3769
|
+
// Rich text editors like Trix bind ⌘K to "insert link"; capturing at the
|
|
3770
|
+
// window and stopping propagation keeps them from ever seeing it.
|
|
3370
3771
|
const handleKeyDown = event => {
|
|
3371
|
-
if (
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
event
|
|
3376
|
-
|
|
3772
|
+
if (isSearchShortcut(event)) {
|
|
3773
|
+
event.preventDefault();
|
|
3774
|
+
event.stopPropagation();
|
|
3775
|
+
setOpen(wasOpen => !wasOpen);
|
|
3776
|
+
} else if (isLegacySearchShortcut(event)) {
|
|
3777
|
+
event.preventDefault();
|
|
3778
|
+
event.stopPropagation();
|
|
3779
|
+
setOpen(true);
|
|
3780
|
+
}
|
|
3377
3781
|
};
|
|
3378
3782
|
window.addEventListener("keydown", handleKeyDown, {
|
|
3379
3783
|
capture: true
|
|
@@ -3383,7 +3787,7 @@ true;
|
|
|
3383
3787
|
});
|
|
3384
3788
|
}, []);
|
|
3385
3789
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("button", {
|
|
3386
|
-
"aria-keyshortcuts": isMac() ? "Meta+K" : "Control+K"
|
|
3790
|
+
"aria-keyshortcuts": `${isMac() ? "Meta+K" : "Control+K"} /`,
|
|
3387
3791
|
"aria-label": "Search",
|
|
3388
3792
|
onClick: () => setOpen(true),
|
|
3389
3793
|
type: "button"
|
|
@@ -3394,21 +3798,357 @@ true;
|
|
|
3394
3798
|
dismissible: true,
|
|
3395
3799
|
onClose: () => setOpen(false),
|
|
3396
3800
|
open: open
|
|
3397
|
-
}, /*#__PURE__*/React.createElement(SearchPanel,
|
|
3801
|
+
}, /*#__PURE__*/React.createElement(SearchPanel, {
|
|
3802
|
+
products: products
|
|
3803
|
+
})));
|
|
3398
3804
|
}
|
|
3399
|
-
function SearchPanel(
|
|
3805
|
+
function SearchPanel({
|
|
3806
|
+
products
|
|
3807
|
+
}) {
|
|
3808
|
+
var _a, _b;
|
|
3809
|
+
const listboxId = useInstanceId("product-search-results");
|
|
3400
3810
|
const [term, setTerm] = React.useState("");
|
|
3811
|
+
const [selectedSlug, setSelectedSlug] = React.useState(() => initialProduct(products).slug);
|
|
3812
|
+
const product = (_a = products.find(({
|
|
3813
|
+
slug
|
|
3814
|
+
}) => slug === selectedSlug)) !== null && _a !== void 0 ? _a : products[0];
|
|
3815
|
+
const hasTerm = term.trim() !== "";
|
|
3816
|
+
const search = useProductSearch(product.slug, term);
|
|
3817
|
+
const groups = "groups" in search ? search.groups : undefined;
|
|
3818
|
+
const results = React.useMemo(() => {
|
|
3819
|
+
var _a;
|
|
3820
|
+
return (_a = groups === null || groups === void 0 ? void 0 : groups.flatMap(group => group.results)) !== null && _a !== void 0 ? _a : [];
|
|
3821
|
+
}, [groups]);
|
|
3822
|
+
const navigation = useResultNavigation(results);
|
|
3823
|
+
const {
|
|
3824
|
+
highlightedIndex
|
|
3825
|
+
} = navigation;
|
|
3826
|
+
const inputRef = React.useRef(null);
|
|
3827
|
+
const [announceHighlight, setAnnounceHighlight] = React.useState(false);
|
|
3828
|
+
// The tabs keep focus while arrowing through results, but only the focused
|
|
3829
|
+
// input can point screen readers at the highlight, so from the tabs it's
|
|
3830
|
+
// announced instead. Typing goes back to the input, keystroke and all.
|
|
3831
|
+
const handleTabsKeyDown = event => {
|
|
3832
|
+
var _a;
|
|
3833
|
+
if (isTypingKey(event)) {
|
|
3834
|
+
(_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
3835
|
+
} else if (verticalStep(event.key)) {
|
|
3836
|
+
setAnnounceHighlight(true);
|
|
3837
|
+
navigation.handleKeyDown(event);
|
|
3838
|
+
} else {
|
|
3839
|
+
navigation.handleKeyDown(event);
|
|
3840
|
+
}
|
|
3841
|
+
};
|
|
3842
|
+
React.useEffect(() => {
|
|
3843
|
+
if (hasTerm) saveLastProduct(product.slug);
|
|
3844
|
+
}, [hasTerm, product.slug]);
|
|
3401
3845
|
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement("input", {
|
|
3846
|
+
"aria-activedescendant": highlightedIndex === null ? undefined : resultId(listboxId, highlightedIndex),
|
|
3847
|
+
"aria-autocomplete": "list",
|
|
3848
|
+
"aria-controls": results.length > 0 ? listboxId : undefined,
|
|
3849
|
+
"aria-expanded": results.length > 0,
|
|
3402
3850
|
"aria-label": "Search",
|
|
3403
3851
|
autoFocus: true,
|
|
3404
3852
|
onChange: event => setTerm(event.target.value),
|
|
3853
|
+
onFocus: () => setAnnounceHighlight(false),
|
|
3854
|
+
onKeyDown: navigation.handleKeyDown,
|
|
3405
3855
|
placeholder: "Search",
|
|
3856
|
+
ref: inputRef,
|
|
3857
|
+
role: "combobox",
|
|
3406
3858
|
type: "search",
|
|
3407
3859
|
value: term
|
|
3408
|
-
}), /*#__PURE__*/React.createElement(
|
|
3860
|
+
}), /*#__PURE__*/React.createElement(ProductTabs, {
|
|
3861
|
+
onKeyDown: handleTabsKeyDown,
|
|
3862
|
+
onSelect: setSelectedSlug,
|
|
3863
|
+
products: products,
|
|
3864
|
+
selected: product
|
|
3865
|
+
}, /*#__PURE__*/React.createElement(SearchResults, {
|
|
3866
|
+
highlightedIndex: highlightedIndex,
|
|
3867
|
+
id: listboxId,
|
|
3868
|
+
label: `${product.appName} results`,
|
|
3869
|
+
onHighlight: navigation.highlightIndex,
|
|
3870
|
+
search: search,
|
|
3409
3871
|
term: term
|
|
3872
|
+
})), /*#__PURE__*/React.createElement("div", {
|
|
3873
|
+
"aria-live": "polite",
|
|
3874
|
+
className: styles$c.visuallyHidden
|
|
3875
|
+
}, announceHighlight ? (_b = navigation.highlighted) === null || _b === void 0 ? void 0 : _b.attributes.title : null), /*#__PURE__*/React.createElement(KeyboardHints, {
|
|
3876
|
+
copied: navigation.copied
|
|
3410
3877
|
}));
|
|
3411
3878
|
}
|
|
3879
|
+
function KeyboardHints({
|
|
3880
|
+
copied
|
|
3881
|
+
}) {
|
|
3882
|
+
const command = commandKeyLabel();
|
|
3883
|
+
return /*#__PURE__*/React.createElement("p", null, "\u2191\u2193 Select \u00B7 \u21B5 Open \u00B7 ", command, "\u21B5 Open in new tab \u00B7 ", command, "C", " ", /*#__PURE__*/React.createElement("span", {
|
|
3884
|
+
role: "status"
|
|
3885
|
+
}, copied ? "Copied!" : "Copy link"), " \u00B7 ", command, "K Open search");
|
|
3886
|
+
}
|
|
3887
|
+
function initialProduct(products) {
|
|
3888
|
+
var _a, _b, _c;
|
|
3889
|
+
const lastProduct = readLastProduct();
|
|
3890
|
+
const bySlug = slug => products.find(product => product.slug === slug);
|
|
3891
|
+
return (_c = (_b = (_a = bySlug(lastProduct)) !== null && _a !== void 0 ? _a : bySlug("people")) !== null && _b !== void 0 ? _b : bySlug("services")) !== null && _c !== void 0 ? _c : products[0];
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
function parseValue(element) {
|
|
3895
|
+
const datatype = element.getAttribute("datatype");
|
|
3896
|
+
switch (datatype) {
|
|
3897
|
+
case "boolean":
|
|
3898
|
+
return element.content === "true" || element.content === "1";
|
|
3899
|
+
default:
|
|
3900
|
+
return element.content;
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
function getAllPcoMeta() {
|
|
3904
|
+
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
3905
|
+
const meta = {};
|
|
3906
|
+
elements.forEach(e => {
|
|
3907
|
+
const property = e.getAttribute("property");
|
|
3908
|
+
meta[property.substring("pco:".length)] = parseValue(e);
|
|
3909
|
+
});
|
|
3910
|
+
return meta;
|
|
3911
|
+
}
|
|
3912
|
+
// usePcoMeta only reads each tag's content. Recently Visited also needs the
|
|
3913
|
+
// pco:recently_visitable tag's `metadata` attribute, which can change while
|
|
3914
|
+
// the content stays the same, so it is tracked in its own store.
|
|
3915
|
+
function getRecentlyVisitableMetadata() {
|
|
3916
|
+
var _a, _b;
|
|
3917
|
+
return (_b = (_a = document.head.querySelector(`meta[property="pco:recently_visitable"]`)) === null || _a === void 0 ? void 0 : _a.getAttribute("metadata")) !== null && _b !== void 0 ? _b : null;
|
|
3918
|
+
}
|
|
3919
|
+
const store = createStore(getAllPcoMeta());
|
|
3920
|
+
const recentlyVisitableMetadataStore = createStore({
|
|
3921
|
+
metadata: getRecentlyVisitableMetadata()
|
|
3922
|
+
});
|
|
3923
|
+
function sync() {
|
|
3924
|
+
const nextState = getAllPcoMeta();
|
|
3925
|
+
if (!shallowCompare(store.getState(), nextState)) store.setState(nextState);
|
|
3926
|
+
const metadata = getRecentlyVisitableMetadata();
|
|
3927
|
+
if (recentlyVisitableMetadataStore.getState().metadata !== metadata) {
|
|
3928
|
+
recentlyVisitableMetadataStore.setState({
|
|
3929
|
+
metadata
|
|
3930
|
+
});
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
const observer = new MutationObserver(sync);
|
|
3934
|
+
let hasTriggeredObserve = false;
|
|
3935
|
+
function observe() {
|
|
3936
|
+
if (hasTriggeredObserve) return;
|
|
3937
|
+
hasTriggeredObserve = true;
|
|
3938
|
+
// The module-load snapshot above can predate meta tags that appear later
|
|
3939
|
+
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
3940
|
+
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
3941
|
+
// never mutate afterward, so the observer alone would never catch them.
|
|
3942
|
+
// Re-read once now, since observe() is only called once a consumer is
|
|
3943
|
+
// actually mounting, which is always after the document has fully parsed.
|
|
3944
|
+
sync();
|
|
3945
|
+
observer.observe(document.head, {
|
|
3946
|
+
attributes: true,
|
|
3947
|
+
childList: true,
|
|
3948
|
+
subtree: true
|
|
3949
|
+
});
|
|
3950
|
+
}
|
|
3951
|
+
function usePcoMeta() {
|
|
3952
|
+
observe();
|
|
3953
|
+
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
3954
|
+
}
|
|
3955
|
+
function useRecentlyVisitableMetadata() {
|
|
3956
|
+
observe();
|
|
3957
|
+
return shim.useSyncExternalStore(recentlyVisitableMetadataStore.subscribe, () => recentlyVisitableMetadataStore.getState().metadata);
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
const MAX_ENTRIES = 20;
|
|
3961
|
+
const FORMAT_VERSION = 1;
|
|
3962
|
+
const MAX_TEXT_LENGTH = 200;
|
|
3963
|
+
const PRODUCTS = ["accounts", "calendar", "check-ins", "giving", "groups", "home", "login", "people", "publishing", "registrations", "services"];
|
|
3964
|
+
function isId(value) {
|
|
3965
|
+
return value !== "" && Array.from(value).every(character => character >= "0" && character <= "9");
|
|
3966
|
+
}
|
|
3967
|
+
// A PCRL looks like `<product>/v2/<resource_type>/<id>`.
|
|
3968
|
+
function isPcrl(value) {
|
|
3969
|
+
const segments = value.split("/");
|
|
3970
|
+
return segments.length === 4 && PRODUCTS.some(product => product === segments[0]) && isId(segments[3]);
|
|
3971
|
+
}
|
|
3972
|
+
function isNonBlankString(value) {
|
|
3973
|
+
return typeof value === "string" && value.trim() !== "";
|
|
3974
|
+
}
|
|
3975
|
+
function isRecord(value) {
|
|
3976
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3977
|
+
}
|
|
3978
|
+
function entryKey(entry) {
|
|
3979
|
+
if (entry.type === "resource") {
|
|
3980
|
+
return `resource:${entry.pcrl}`;
|
|
3981
|
+
} else {
|
|
3982
|
+
return `page:${entry.product}:${entry.path}`;
|
|
3983
|
+
}
|
|
3984
|
+
}
|
|
3985
|
+
function isEntry(value) {
|
|
3986
|
+
if (!isRecord(value)) {
|
|
3987
|
+
return false;
|
|
3988
|
+
} else if (value.type === "resource") {
|
|
3989
|
+
return typeof value.pcrl === "string" && isPcrl(value.pcrl);
|
|
3990
|
+
} else if (value.type === "page") {
|
|
3991
|
+
return isNonBlankString(value.product) && isNonBlankString(value.title) && isNonBlankString(value.path) && typeof value.details === "string";
|
|
3992
|
+
} else {
|
|
3993
|
+
return false;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
// Cuts on code points so a surrogate pair is never split.
|
|
3997
|
+
function clampText(value) {
|
|
3998
|
+
return Array.from(value).slice(0, MAX_TEXT_LENGTH).join("");
|
|
3999
|
+
}
|
|
4000
|
+
function clampEntry(entry) {
|
|
4001
|
+
if (entry.type === "resource") {
|
|
4002
|
+
return entry;
|
|
4003
|
+
} else {
|
|
4004
|
+
return {
|
|
4005
|
+
...entry,
|
|
4006
|
+
details: clampText(entry.details),
|
|
4007
|
+
title: clampText(entry.title)
|
|
4008
|
+
};
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
// Recents are disposable: anything unexpected reads as an empty list (or, for
|
|
4012
|
+
// a single bad entry, a list without it) rather than throwing.
|
|
4013
|
+
function parseRecentlyVisited(value, personId) {
|
|
4014
|
+
let parsed = null;
|
|
4015
|
+
try {
|
|
4016
|
+
parsed = JSON.parse(value !== null && value !== void 0 ? value : "null");
|
|
4017
|
+
} catch {
|
|
4018
|
+
parsed = null;
|
|
4019
|
+
}
|
|
4020
|
+
if (isRecord(parsed) && parsed.v === FORMAT_VERSION && parsed.person === personId && Array.isArray(parsed.entries)) {
|
|
4021
|
+
return parsed.entries.filter(isEntry);
|
|
4022
|
+
} else {
|
|
4023
|
+
return [];
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
function addVisit(entries, visit) {
|
|
4027
|
+
const key = entryKey(visit);
|
|
4028
|
+
return [clampEntry(visit), ...entries.filter(entry => entryKey(entry) !== key)].slice(0, MAX_ENTRIES);
|
|
4029
|
+
}
|
|
4030
|
+
function serializeRecentlyVisited(entries, personId) {
|
|
4031
|
+
return JSON.stringify({
|
|
4032
|
+
entries,
|
|
4033
|
+
person: personId,
|
|
4034
|
+
v: FORMAT_VERSION
|
|
4035
|
+
});
|
|
4036
|
+
}
|
|
4037
|
+
|
|
4038
|
+
// The product whose URL, in this environment, has the current page's host.
|
|
4039
|
+
// Reversing pcoUrl handles every environment @planningcenter/url knows about,
|
|
4040
|
+
// and guarantees the stored product rebuilds a working link.
|
|
4041
|
+
function currentProduct() {
|
|
4042
|
+
var _a;
|
|
4043
|
+
return (_a = PRODUCTS.find(product => new URL(pcoUrl$1.pcoUrl(product)).hostname === window.location.hostname)) !== null && _a !== void 0 ? _a : null;
|
|
4044
|
+
}
|
|
4045
|
+
function detailsFromMetadata(metadata) {
|
|
4046
|
+
var _a;
|
|
4047
|
+
try {
|
|
4048
|
+
const details = (_a = JSON.parse(metadata !== null && metadata !== void 0 ? metadata : "{}")) === null || _a === void 0 ? void 0 : _a.details;
|
|
4049
|
+
return typeof details === "string" ? details : "";
|
|
4050
|
+
} catch {
|
|
4051
|
+
return "";
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
// pco:resource is the more specific signal, so it wins when both tags are
|
|
4055
|
+
// present. A resource that isn't a PCRL is ignored rather than recorded.
|
|
4056
|
+
function visitFromPage({
|
|
4057
|
+
metadata,
|
|
4058
|
+
resource,
|
|
4059
|
+
title
|
|
4060
|
+
}) {
|
|
4061
|
+
const product = currentProduct();
|
|
4062
|
+
if (resource && isPcrl(resource)) {
|
|
4063
|
+
return {
|
|
4064
|
+
pcrl: resource,
|
|
4065
|
+
type: "resource"
|
|
4066
|
+
};
|
|
4067
|
+
} else if (title && title.trim() !== "" && product) {
|
|
4068
|
+
return {
|
|
4069
|
+
details: detailsFromMetadata(metadata),
|
|
4070
|
+
path: window.location.pathname,
|
|
4071
|
+
product,
|
|
4072
|
+
title,
|
|
4073
|
+
type: "page"
|
|
4074
|
+
};
|
|
4075
|
+
} else {
|
|
4076
|
+
return null;
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
// The entry Recently Visited would record for the current page, or null when
|
|
4080
|
+
// the page isn't eligible. Shared by the recorder and the debug panel so the
|
|
4081
|
+
// panel always shows what actually gets stored.
|
|
4082
|
+
function useCurrentVisit() {
|
|
4083
|
+
const {
|
|
4084
|
+
recently_visitable: title,
|
|
4085
|
+
resource
|
|
4086
|
+
} = usePcoMeta();
|
|
4087
|
+
const metadata = useRecentlyVisitableMetadata();
|
|
4088
|
+
return React.useMemo(() => visitFromPage({
|
|
4089
|
+
metadata,
|
|
4090
|
+
resource,
|
|
4091
|
+
title
|
|
4092
|
+
}), [metadata, resource, title]);
|
|
4093
|
+
}
|
|
4094
|
+
// Products hand the person's ID to AppsProvider, not to the topbar itself,
|
|
4095
|
+
// and several pass it as a number.
|
|
4096
|
+
function useCurrentPersonId() {
|
|
4097
|
+
const {
|
|
4098
|
+
userId
|
|
4099
|
+
} = useStore();
|
|
4100
|
+
return userId == null ? undefined : String(userId);
|
|
4101
|
+
}
|
|
4102
|
+
|
|
4103
|
+
// Per-origin localStorage is a stopgap: each product keeps its own list until
|
|
4104
|
+
// PCA-409 moves this to storage shared across product subdomains. Only this
|
|
4105
|
+
// file should need to change when it does.
|
|
4106
|
+
function storageKey({
|
|
4107
|
+
env,
|
|
4108
|
+
personId
|
|
4109
|
+
}) {
|
|
4110
|
+
return `topbar.recently_visited.${env}.${personId}`;
|
|
4111
|
+
}
|
|
4112
|
+
// localStorage doesn't notify the tab that wrote it, so readers in this tab
|
|
4113
|
+
// learn about new visits from this counter instead.
|
|
4114
|
+
const changes = createStore({
|
|
4115
|
+
version: 0
|
|
4116
|
+
});
|
|
4117
|
+
function readRecentlyVisited(scope) {
|
|
4118
|
+
try {
|
|
4119
|
+
return parseRecentlyVisited(localStorage.getItem(storageKey(scope)), scope.personId);
|
|
4120
|
+
} catch {
|
|
4121
|
+
return [];
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
function recordVisit(visit, scope) {
|
|
4125
|
+
if (isEntry(visit)) {
|
|
4126
|
+
try {
|
|
4127
|
+
localStorage.setItem(storageKey(scope), serializeRecentlyVisited(addVisit(readRecentlyVisited(scope), visit), scope.personId));
|
|
4128
|
+
changes.setState({
|
|
4129
|
+
version: changes.getState().version + 1
|
|
4130
|
+
});
|
|
4131
|
+
} catch {
|
|
4132
|
+
// Storage can be full or blocked; recents are disposable.
|
|
4133
|
+
}
|
|
4134
|
+
}
|
|
4135
|
+
}
|
|
4136
|
+
|
|
4137
|
+
function useRecordRecentlyVisited({
|
|
4138
|
+
enabled,
|
|
4139
|
+
env
|
|
4140
|
+
}) {
|
|
4141
|
+
const personId = useCurrentPersonId();
|
|
4142
|
+
const visit = useCurrentVisit();
|
|
4143
|
+
React.useEffect(() => {
|
|
4144
|
+
if (enabled && personId && visit) {
|
|
4145
|
+
recordVisit(visit, {
|
|
4146
|
+
env,
|
|
4147
|
+
personId
|
|
4148
|
+
});
|
|
4149
|
+
}
|
|
4150
|
+
}, [enabled, env, personId, visit]);
|
|
4151
|
+
}
|
|
3412
4152
|
|
|
3413
4153
|
function StyledRoot({
|
|
3414
4154
|
children,
|
|
@@ -3638,6 +4378,10 @@ true;
|
|
|
3638
4378
|
const updateStore = useUpdateStore();
|
|
3639
4379
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
3640
4380
|
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
4381
|
+
useRecordRecentlyVisited({
|
|
4382
|
+
enabled: productSearchEnabled,
|
|
4383
|
+
env: props.env
|
|
4384
|
+
});
|
|
3641
4385
|
React.useEffect(() => {
|
|
3642
4386
|
const handleToolbarClick = event => {
|
|
3643
4387
|
const target = event.target;
|
|
@@ -3678,7 +4422,9 @@ true;
|
|
|
3678
4422
|
style: {
|
|
3679
4423
|
margin: "auto"
|
|
3680
4424
|
}
|
|
3681
|
-
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch,
|
|
4425
|
+
}), !notificationsEnabled && notifications(), productSearchEnabled ? (/*#__PURE__*/React.createElement(ProductSearch, {
|
|
4426
|
+
apps: props.apps
|
|
4427
|
+
})) : search({
|
|
3682
4428
|
hideRoutes: () => setRoutesVisible(false),
|
|
3683
4429
|
showRoutes: () => setRoutesVisible(true)
|
|
3684
4430
|
}), /*#__PURE__*/React.createElement(UserMenuTrigger, {
|
|
@@ -31754,6 +32500,11 @@ true;
|
|
|
31754
32500
|
const [routesMenuVisible, setRoutesMenuVisible] = React.useState(false);
|
|
31755
32501
|
const [productSwitcherVisible, setProductSwitcherVisible] = React.useState(false);
|
|
31756
32502
|
const useRedesignedSwitcher = useFeature(FEATURE_FLAGS.APP_SWITCHER_REDESIGN, props.features);
|
|
32503
|
+
const productSearchEnabled = useFeature(FEATURE_FLAGS.PRODUCT_SEARCH, props.features);
|
|
32504
|
+
useRecordRecentlyVisited({
|
|
32505
|
+
enabled: productSearchEnabled,
|
|
32506
|
+
env: props.env
|
|
32507
|
+
});
|
|
31757
32508
|
const activeRoute = props.routes.filter(({
|
|
31758
32509
|
props: {
|
|
31759
32510
|
active
|
|
@@ -31962,55 +32713,6 @@ true;
|
|
|
31962
32713
|
id: "topbar"
|
|
31963
32714
|
};
|
|
31964
32715
|
|
|
31965
|
-
function parseValue(element) {
|
|
31966
|
-
const datatype = element.getAttribute("datatype");
|
|
31967
|
-
switch (datatype) {
|
|
31968
|
-
case "boolean":
|
|
31969
|
-
return element.content === "true" || element.content === "1";
|
|
31970
|
-
default:
|
|
31971
|
-
return element.content;
|
|
31972
|
-
}
|
|
31973
|
-
}
|
|
31974
|
-
function getAllPcoMeta() {
|
|
31975
|
-
const elements = document.head.querySelectorAll(`meta[property^="pco:"]`);
|
|
31976
|
-
const meta = {};
|
|
31977
|
-
elements.forEach(e => {
|
|
31978
|
-
const property = e.getAttribute("property");
|
|
31979
|
-
meta[property.substring("pco:".length)] = parseValue(e);
|
|
31980
|
-
});
|
|
31981
|
-
return meta;
|
|
31982
|
-
}
|
|
31983
|
-
const store = createStore(getAllPcoMeta());
|
|
31984
|
-
const observer = new MutationObserver(() => {
|
|
31985
|
-
const prevState = store.getState();
|
|
31986
|
-
const nextState = getAllPcoMeta();
|
|
31987
|
-
if (!shallowCompare(prevState, nextState)) store.setState(nextState);
|
|
31988
|
-
});
|
|
31989
|
-
let hasTriggeredObserve = false;
|
|
31990
|
-
function observe() {
|
|
31991
|
-
if (hasTriggeredObserve) return;
|
|
31992
|
-
hasTriggeredObserve = true;
|
|
31993
|
-
// The module-load snapshot above can predate meta tags that appear later
|
|
31994
|
-
// in <head> (e.g. pco:resource, yielded right before </head>) if this
|
|
31995
|
-
// module's own script tag isn't deferred and runs mid-parse. Those tags
|
|
31996
|
-
// never mutate afterward, so the observer alone would never catch them.
|
|
31997
|
-
// Re-read once now, since observe() is only called once a consumer is
|
|
31998
|
-
// actually mounting, which is always after the document has fully parsed.
|
|
31999
|
-
const freshState = getAllPcoMeta();
|
|
32000
|
-
if (!shallowCompare(store.getState(), freshState)) {
|
|
32001
|
-
store.setState(freshState);
|
|
32002
|
-
}
|
|
32003
|
-
observer.observe(document.head, {
|
|
32004
|
-
attributes: true,
|
|
32005
|
-
childList: true,
|
|
32006
|
-
subtree: true
|
|
32007
|
-
});
|
|
32008
|
-
}
|
|
32009
|
-
function usePcoMeta() {
|
|
32010
|
-
observe();
|
|
32011
|
-
return shim.useSyncExternalStore(store.subscribe, () => store.getState());
|
|
32012
|
-
}
|
|
32013
|
-
|
|
32014
32716
|
var styles$4 = {"debug-pco-resource":"debug-pco-resource-sLteM","debug-pco-resource-header":"debug-pco-resource-header-5fEbe","debug-pco-resource-badge":"debug-pco-resource-badge-J9v1F","debug-pco-resource-title":"debug-pco-resource-title-volWV","debug-pco-resource-field":"debug-pco-resource-field-7Gitp","debug-pco-resource-field-label":"debug-pco-resource-field-label--f1xX","debug-pco-resource-field-value":"debug-pco-resource-field-value-YoX-C","debug-pco-resource-note":"debug-pco-resource-note-N5xyf","debug-pco-resource-json":"debug-pco-resource-json-Rx-JI"};
|
|
32015
32717
|
|
|
32016
32718
|
function isPresent(value) {
|