@favish/staffbase-utils 0.11.0 → 0.12.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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/dist/links/react.cjs.js +2 -0
  3. package/dist/links/react.cjs.js.map +1 -0
  4. package/dist/links/react.es.mjs +75 -0
  5. package/dist/links/react.es.mjs.map +1 -0
  6. package/dist/links.cjs.js +1 -1
  7. package/dist/links.cjs.js.map +1 -1
  8. package/dist/links.es.mjs +10 -81
  9. package/dist/links.es.mjs.map +1 -1
  10. package/dist/openStaffbaseAware-CJs5uAvX.mjs +78 -0
  11. package/dist/openStaffbaseAware-CJs5uAvX.mjs.map +1 -0
  12. package/dist/openStaffbaseAware-CWYBlqcd.js +2 -0
  13. package/dist/openStaffbaseAware-CWYBlqcd.js.map +1 -0
  14. package/dist/src/links/react/hasMouseEventProperties.d.ts +8 -0
  15. package/dist/src/links/react/hasMouseEventProperties.d.ts.map +1 -0
  16. package/dist/src/links/react/index.d.ts +4 -0
  17. package/dist/src/links/react/index.d.ts.map +1 -0
  18. package/dist/src/links/react/isModifiedNativeEvent.d.ts +9 -0
  19. package/dist/src/links/react/isModifiedNativeEvent.d.ts.map +1 -0
  20. package/dist/src/links/react/isNativeLinkEvent.d.ts +12 -0
  21. package/dist/src/links/react/isNativeLinkEvent.d.ts.map +1 -0
  22. package/dist/src/links/react/isTouchLinkEvent.d.ts +8 -0
  23. package/dist/src/links/react/isTouchLinkEvent.d.ts.map +1 -0
  24. package/dist/src/links/react/nativeLinkHandlers.d.ts +7 -0
  25. package/dist/src/links/react/nativeLinkHandlers.d.ts.map +1 -0
  26. package/dist/src/links/react/tryStaffbaseContentOpenLink.d.ts +10 -0
  27. package/dist/src/links/react/tryStaffbaseContentOpenLink.d.ts.map +1 -0
  28. package/dist/src/links/react/useInAppLinkHandling.d.ts +15 -0
  29. package/dist/src/links/react/useInAppLinkHandling.d.ts.map +1 -0
  30. package/dist/src/types/links/NativeLinkEvent.d.ts +6 -0
  31. package/dist/src/types/links/NativeLinkEvent.d.ts.map +1 -0
  32. package/dist/src/types/links/StaffbaseContentWindow.d.ts +35 -0
  33. package/dist/src/types/links/StaffbaseContentWindow.d.ts.map +1 -0
  34. package/dist/src/types/links/UseInAppLinkHandlingOptions.d.ts +19 -0
  35. package/dist/src/types/links/UseInAppLinkHandlingOptions.d.ts.map +1 -0
  36. package/dist/src/types/links/UseInAppLinkHandlingReturn.d.ts +31 -0
  37. package/dist/src/types/links/UseInAppLinkHandlingReturn.d.ts.map +1 -0
  38. package/package.json +6 -1
package/README.md CHANGED
@@ -25,6 +25,7 @@ minimumReleaseAgeExclude:
25
25
  | --- | --- |
26
26
  | `@favish/staffbase-utils/api` | `fetchJson`, `fetchAllPaginated`, `ApiError` |
27
27
  | `@favish/staffbase-utils/content` | `resolveLocalizedContent`, `resolveActiveLanguage`, `detectEditorLanguage`, `detectPreviewLanguage` |
28
+ | `@favish/staffbase-utils/links/react` | `useInAppLinkHandling` (React peer) |
28
29
  | `@favish/staffbase-utils/log` | `logError`, `logWarn`, `logDebug`, `setLoggingEnabled` |
29
30
  | `@favish/staffbase-utils/dom` | `getDynamicClasses` |
30
31
  | `@favish/staffbase-utils/types` | `Channel`, `ChannelLink`, `ChannelLinkParameter`, `DropdownOption`, `LocalizedContent`, `ArticleImage`, `ArticleImageVariant` (type-only) |
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../openStaffbaseAware-CWYBlqcd.js");let t=require("react");var n=e=>`button`in e&&`metaKey`in e,r=e=>{let t=n(e),r=t&&`button`in e?e.button:void 0,i=t?e.metaKey:!1,a=t?e.ctrlKey:!1,o=t?e.shiftKey:!1,s=t?e.altKey:!1;return!!(e.defaultPrevented||typeof r==`number`&&r!==0||i||a||o||s)},i=e=>e!==void 0&&`target`in e&&e instanceof Event,a=e=>`changedTouches`in e,o=new WeakMap,s=e=>{let t=window.staffbase,n=t?.content?.link?.openLink||t?.content?.links?.openLink||t?.content?.loader?.link?.openLink;if(typeof n!=`function`)return!1;try{return n({useDefault:!1},e),!0}catch{return!1}},c=700,l=({staffbaseOrigin:n,onAfterOpen:l,containerRef:u})=>{let d=(0,t.useRef)(null),f=(0,t.useRef)(null),p=(0,t.useRef)(null),m=(0,t.useRef)(null),h=(0,t.useCallback)(t=>(e.r(t,n),t),[n]),g=(0,t.useCallback)((t,r)=>{let o=e.a(t.getAttribute(`href`)??t.href??null,n);if(o){if(r&&`preventDefault`in r&&(r.preventDefault?.(),r.stopPropagation?.()),i(r)){if(a(r)){e.t(o),l?.();return}s(r)||e.t(o)}else e.t(o);l?.()}},[l,n]),_=(0,t.useCallback)(e=>{if(e.defaultPrevented||d.current&&Date.now()-d.current<c)return;let t=e.target;if(!(t instanceof Element))return;let n=t.closest(`a`);n&&g(n,e)},[g]),v=(0,t.useCallback)(e=>{if(e.defaultPrevented)return;let t=e.target;if(!(t instanceof Element))return;let n=t.closest(`a`);n&&(f.current&&p.current===`pointer`&&m.current===n&&Date.now()-f.current<c||(d.current=Date.now(),f.current=d.current,p.current=`touch`,m.current=n,g(n,e.nativeEvent)))},[g]),y=(0,t.useCallback)(e=>{if(e.defaultPrevented)return;let t=e.target;if(!(t instanceof Element))return;let n=t.closest(`a`);n&&(f.current&&p.current===`touch`&&m.current===n&&Date.now()-f.current<c||(d.current=Date.now(),f.current=d.current,p.current=`pointer`,m.current=n,g(n,e.nativeEvent)))},[g]);return(0,t.useEffect)(()=>{let e=u?.current;if(!e||o.has(e))return;let t=e=>{try{if(r(e))return;let t=e.target;if(!(t instanceof Element))return;let n=t.closest(`a`);if(!n)return;let i=Date.now();if(f.current&&m.current===n&&i-f.current<c)return;f.current=i,p.current=a(e)?`touch`:`pointer`,m.current=n,g(n,e)}catch{}};return o.set(e,t),e.addEventListener(`click`,t,!0),e.addEventListener(`touchend`,t,!0),e.addEventListener(`pointerup`,t,!0),()=>{e.removeEventListener(`click`,t,!0),e.removeEventListener(`touchend`,t,!0),e.removeEventListener(`pointerup`,t,!0),o.delete(e)}},[u,g]),{prepareHtmlContent:h,handleContentClick:_,handleContentTouchEnd:v,handleContentPointerUp:y}};exports.useInAppLinkHandling=l;
2
+ //# sourceMappingURL=react.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.cjs.js","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import { a as e, r as t, t as n } from "../openStaffbaseAware-CJs5uAvX.mjs";
2
+ import { useCallback as r, useEffect as i, useRef as a } from "react";
3
+ //#region src/links/react/hasMouseEventProperties.ts
4
+ var o = (e) => "button" in e && "metaKey" in e, s = (e) => {
5
+ let t = o(e), n = t && "button" in e ? e.button : void 0, r = t ? e.metaKey : !1, i = t ? e.ctrlKey : !1, a = t ? e.shiftKey : !1, s = t ? e.altKey : !1;
6
+ return !!(e.defaultPrevented || typeof n == "number" && n !== 0 || r || i || a || s);
7
+ }, c = (e) => e !== void 0 && "target" in e && e instanceof Event, l = (e) => "changedTouches" in e, u = /* @__PURE__ */ new WeakMap(), d = (e) => {
8
+ let t = window.staffbase, n = t?.content?.link?.openLink || t?.content?.links?.openLink || t?.content?.loader?.link?.openLink;
9
+ if (typeof n != "function") return !1;
10
+ try {
11
+ return n({ useDefault: !1 }, e), !0;
12
+ } catch {
13
+ return !1;
14
+ }
15
+ }, f = 700, p = ({ staffbaseOrigin: o, onAfterOpen: p, containerRef: m }) => {
16
+ let h = a(null), g = a(null), _ = a(null), v = a(null), y = r((e) => (t(e, o), e), [o]), b = r((t, r) => {
17
+ let i = e(t.getAttribute("href") ?? t.href ?? null, o);
18
+ if (i) {
19
+ if (r && "preventDefault" in r && (r.preventDefault?.(), r.stopPropagation?.()), c(r)) {
20
+ if (l(r)) {
21
+ n(i), p?.();
22
+ return;
23
+ }
24
+ d(r) || n(i);
25
+ } else n(i);
26
+ p?.();
27
+ }
28
+ }, [p, o]), x = r((e) => {
29
+ if (e.defaultPrevented || h.current && Date.now() - h.current < f) return;
30
+ let t = e.target;
31
+ if (!(t instanceof Element)) return;
32
+ let n = t.closest("a");
33
+ n && b(n, e);
34
+ }, [b]), S = r((e) => {
35
+ if (e.defaultPrevented) return;
36
+ let t = e.target;
37
+ if (!(t instanceof Element)) return;
38
+ let n = t.closest("a");
39
+ n && (g.current && _.current === "pointer" && v.current === n && Date.now() - g.current < f || (h.current = Date.now(), g.current = h.current, _.current = "touch", v.current = n, b(n, e.nativeEvent)));
40
+ }, [b]), C = r((e) => {
41
+ if (e.defaultPrevented) return;
42
+ let t = e.target;
43
+ if (!(t instanceof Element)) return;
44
+ let n = t.closest("a");
45
+ n && (g.current && _.current === "touch" && v.current === n && Date.now() - g.current < f || (h.current = Date.now(), g.current = h.current, _.current = "pointer", v.current = n, b(n, e.nativeEvent)));
46
+ }, [b]);
47
+ return i(() => {
48
+ let e = m?.current;
49
+ if (!e || u.has(e)) return;
50
+ let t = (e) => {
51
+ try {
52
+ if (s(e)) return;
53
+ let t = e.target;
54
+ if (!(t instanceof Element)) return;
55
+ let n = t.closest("a");
56
+ if (!n) return;
57
+ let r = Date.now();
58
+ if (g.current && v.current === n && r - g.current < f) return;
59
+ g.current = r, _.current = l(e) ? "touch" : "pointer", v.current = n, b(n, e);
60
+ } catch {}
61
+ };
62
+ return u.set(e, t), e.addEventListener("click", t, !0), e.addEventListener("touchend", t, !0), e.addEventListener("pointerup", t, !0), () => {
63
+ e.removeEventListener("click", t, !0), e.removeEventListener("touchend", t, !0), e.removeEventListener("pointerup", t, !0), u.delete(e);
64
+ };
65
+ }, [m, b]), {
66
+ prepareHtmlContent: y,
67
+ handleContentClick: x,
68
+ handleContentTouchEnd: S,
69
+ handleContentPointerUp: C
70
+ };
71
+ };
72
+ //#endregion
73
+ export { p as useInAppLinkHandling };
74
+
75
+ //# sourceMappingURL=react.es.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.es.mjs","names":[],"sources":["../../src/links/react/hasMouseEventProperties.ts","../../src/links/react/isModifiedNativeEvent.ts","../../src/links/react/isNativeLinkEvent.ts","../../src/links/react/isTouchLinkEvent.ts","../../src/links/react/nativeLinkHandlers.ts","../../src/links/react/tryStaffbaseContentOpenLink.ts","../../src/links/react/useInAppLinkHandling.ts"],"sourcesContent":["import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event exposes MouseEvent modifier properties.\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event has `button` and `metaKey`.\n */\nexport const hasMouseEventProperties = (\n e: NativeLinkEvent,\n): e is MouseEvent | PointerEvent => {\n return 'button' in e && 'metaKey' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport { hasMouseEventProperties } from './hasMouseEventProperties'\n\n/**\n * Whether a native link event is \"modified\" and should be left to the browser\n * (default prevented, a non-primary mouse button, or a modifier key held).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event should not be intercepted.\n */\nexport const isModifiedNativeEvent = (e: NativeLinkEvent): boolean => {\n const hasMouseProps = hasMouseEventProperties(e)\n const button = hasMouseProps && 'button' in e ? e.button : undefined\n const metaKey = hasMouseProps ? e.metaKey : false\n const ctrlKey = hasMouseProps ? e.ctrlKey : false\n const shiftKey = hasMouseProps ? e.shiftKey : false\n const altKey = hasMouseProps ? e.altKey : false\n\n return Boolean(\n e.defaultPrevented ||\n (typeof button === 'number' && button !== 0) ||\n metaKey ||\n ctrlKey ||\n shiftKey ||\n altKey,\n )\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a value is a real native DOM event (as opposed to a React synthetic\n * event-like object), so the Staffbase content open-link path can be tried.\n * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.\n * @returns {boolean} True when the value is a native Event.\n */\nexport const isNativeLinkEvent = (\n e:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent\n | undefined,\n): e is NativeLinkEvent => {\n return e !== undefined && 'target' in e && e instanceof Event\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Whether a native link event is a TouchEvent (has `changedTouches`).\n * @param {NativeLinkEvent} e - The native event.\n * @returns {boolean} True when the event is a TouchEvent.\n */\nexport const isTouchLinkEvent = (e: NativeLinkEvent): e is TouchEvent => {\n return 'changedTouches' in e\n}\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\n\n/**\n * Tracks the capture-phase handler installed per container element, so the same\n * container is never wired twice and can be cleanly torn down on unmount.\n */\nexport const nativeLinkHandlers = new WeakMap<\n HTMLElement,\n (e: NativeLinkEvent) => void\n>()\n","import type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { StaffbaseContentWindow } from '../../types/links/StaffbaseContentWindow'\n\n/**\n * Tries the host's own content open-link helper (the one Staffbase's widget\n * manager uses), to stay maximally aligned with platform behavior. Returns false\n * when the helper is absent or throws, so the caller can fall back.\n * @param {NativeLinkEvent} e - The originating native event.\n * @returns {boolean} True when the host helper handled the open.\n */\nexport const tryStaffbaseContentOpenLink = (e: NativeLinkEvent): boolean => {\n const staffbase = (window as unknown as StaffbaseContentWindow).staffbase\n const fn =\n staffbase?.content?.link?.openLink ||\n staffbase?.content?.links?.openLink ||\n staffbase?.content?.loader?.link?.openLink\n\n if (typeof fn !== 'function') return false\n\n try {\n fn({ useDefault: false }, e)\n return true\n } catch {\n // The host helper is best-effort; fall back to standard navigation on error.\n return false\n }\n}\n","import type {\n MouseEvent as ReactMouseEvent,\n PointerEvent as ReactPointerEvent,\n TouchEvent as ReactTouchEvent,\n} from 'react'\nimport { useCallback, useEffect, useRef } from 'react'\n\nimport type { NativeLinkEvent } from '../../types/links/NativeLinkEvent'\nimport type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions'\nimport type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn'\nimport { getInAppOpenLinkTarget } from '../getInAppOpenLinkTarget'\nimport { normalizeInAppLinks } from '../normalizeInAppLinks'\nimport { openStaffbaseAware } from '../openStaffbaseAware'\nimport { isModifiedNativeEvent } from './isModifiedNativeEvent'\nimport { isNativeLinkEvent } from './isNativeLinkEvent'\nimport { isTouchLinkEvent } from './isTouchLinkEvent'\nimport { nativeLinkHandlers } from './nativeLinkHandlers'\nimport { tryStaffbaseContentOpenLink } from './tryStaffbaseContentOpenLink'\n\nconst SUPPRESS_CLICK_WINDOW_MS = 700\n\n/**\n * Centralizes in-content link handling for Staffbase mobile/webview environments.\n *\n * Always attempts Staffbase-aware navigation (the host openLink when available,\n * otherwise same-tab navigation). It exposes React handlers for click/touchend/\n * pointerup and, when `containerRef` is given, installs a more reliable native\n * capture-phase handler. Touch/pointer/click are de-duplicated within a short\n * window so a single tap never fires navigation twice in iOS webviews.\n * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.\n * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.\n */\nexport const useInAppLinkHandling = ({\n staffbaseOrigin,\n onAfterOpen,\n containerRef,\n}: UseInAppLinkHandlingOptions): UseInAppLinkHandlingReturn => {\n const lastTouchAtRef = useRef<number | null>(null)\n const lastHandledAtRef = useRef<number | null>(null)\n const lastHandledTypeRef = useRef<'touch' | 'pointer' | null>(null)\n const lastHandledAnchorRef = useRef<HTMLAnchorElement | null>(null)\n\n const prepareHtmlContent = useCallback(\n (content: HTMLElement): HTMLElement => {\n normalizeInAppLinks(content, staffbaseOrigin)\n return content\n },\n [staffbaseOrigin],\n )\n\n const handleAnchor = useCallback(\n (\n anchor: HTMLAnchorElement,\n e?:\n | { preventDefault?: () => void; stopPropagation?: () => void }\n | NativeLinkEvent,\n ) => {\n const linkToOpen = getInAppOpenLinkTarget(\n anchor.getAttribute('href') ?? anchor.href ?? null,\n staffbaseOrigin,\n )\n if (!linkToOpen) return\n\n // Handle both React synthetic event-like objects and native events.\n if (e && 'preventDefault' in e) {\n e.preventDefault?.()\n e.stopPropagation?.()\n }\n\n // Prefer Staffbase's own content link handler when this is a native event.\n if (isNativeLinkEvent(e)) {\n // Touch events in some webviews don't trigger Staffbase openLink reliably.\n if (isTouchLinkEvent(e)) {\n openStaffbaseAware(linkToOpen)\n onAfterOpen?.()\n return\n }\n if (!tryStaffbaseContentOpenLink(e)) {\n openStaffbaseAware(linkToOpen)\n }\n } else {\n openStaffbaseAware(linkToOpen)\n }\n onAfterOpen?.()\n },\n [onAfterOpen, staffbaseOrigin],\n )\n\n const handleContentClick = useCallback(\n (e: ReactMouseEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n if (\n lastTouchAtRef.current &&\n Date.now() - lastTouchAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n handleAnchor(anchor, e)\n },\n [handleAnchor],\n )\n\n const handleContentTouchEnd = useCallback(\n (e: ReactTouchEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'pointer' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'touch'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n const handleContentPointerUp = useCallback(\n (e: ReactPointerEvent<HTMLElement>) => {\n if (e.defaultPrevented) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n if (\n lastHandledAtRef.current &&\n lastHandledTypeRef.current === 'touch' &&\n lastHandledAnchorRef.current === anchor &&\n Date.now() - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastTouchAtRef.current = Date.now()\n lastHandledAtRef.current = lastTouchAtRef.current\n lastHandledTypeRef.current = 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e.nativeEvent)\n },\n [handleAnchor],\n )\n\n useEffect(() => {\n const container = containerRef?.current\n if (!container) return\n\n if (nativeLinkHandlers.has(container)) return\n\n /**\n * Capture-phase handler intercepting link activations on the container.\n * @param {NativeLinkEvent} e - The native click/touchend/pointerup event.\n * @returns {void} Nothing.\n */\n const handler = (e: NativeLinkEvent) => {\n try {\n if (isModifiedNativeEvent(e)) return\n const target = e.target\n if (!(target instanceof Element)) return\n\n const anchor = target.closest('a')\n if (!anchor) return\n\n const now = Date.now()\n if (\n lastHandledAtRef.current &&\n lastHandledAnchorRef.current === anchor &&\n now - lastHandledAtRef.current < SUPPRESS_CLICK_WINDOW_MS\n ) {\n return\n }\n\n lastHandledAtRef.current = now\n lastHandledTypeRef.current = isTouchLinkEvent(e) ? 'touch' : 'pointer'\n lastHandledAnchorRef.current = anchor\n handleAnchor(anchor, e)\n } catch {\n // Capture-phase handler must never throw into the host event loop.\n }\n }\n\n nativeLinkHandlers.set(container, handler)\n container.addEventListener('click', handler, true)\n container.addEventListener('touchend', handler, true)\n // Pointer events may be enabled in some webviews; harmless if unused.\n container.addEventListener('pointerup', handler, true)\n\n return () => {\n container.removeEventListener('click', handler, true)\n container.removeEventListener('touchend', handler, true)\n container.removeEventListener('pointerup', handler, true)\n nativeLinkHandlers.delete(container)\n }\n }, [containerRef, handleAnchor])\n\n return {\n prepareHtmlContent,\n handleContentClick,\n handleContentTouchEnd,\n handleContentPointerUp,\n }\n}\n"],"mappings":"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"}
package/dist/links.cjs.js CHANGED
@@ -1,2 +1,2 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=e=>e.startsWith(`/deeplink/`)||e.startsWith(`/openlink/`)?`/${e.slice(10)}`:e,t=(t,n)=>{let r=t?.trim();if(!r||r.startsWith(`#`))return null;let i=r.toLowerCase();if(i.startsWith(`mailto:`)||i.startsWith(`tel:`)||i.startsWith(`sms:`)||i.startsWith(`javascript:`)||i.startsWith(`data:`))return null;try{let t=new URL(n),i=new URL(r,t.toString());if(i.origin===t.origin){let n=e(i.pathname);return new URL(`${n}${i.search}${i.hash}`,t.origin).toString()}return i.toString()}catch{return null}},n=[`youtube.com`,`youtube-nocookie.com`,`youtu.be`,`vimeo.com`,`player.vimeo.com`,`staffbase.com`,`staffbase.rocks`],r=e=>{let t=e.trim();if(!t)return!1;try{let e=typeof window<`u`?window.location.origin:`https://localhost`,r=new URL(t,e);if(r.protocol!==`https:`&&r.protocol!==`http:`)return!1;if(r.origin===e)return!0;let i=r.hostname.toLowerCase();return n.some(e=>i===e||i.endsWith(`.${e}`))}catch{return!1}},i=e=>{let t=e.trim().toLowerCase();return t?!(t.startsWith(`javascript:`)||t.startsWith(`data:`)||t.startsWith(`vbscript:`)):!1},a=(e,t)=>{let n=t?.trim()||``;if(!n)return;let r=Array.from(e.querySelectorAll(`a[href]`));for(let e of r){let t=e.getAttribute(`href`);if(!t)continue;let r=t.trim().toLowerCase();if(!(r.startsWith(`#`)||r.startsWith(`mailto:`)||r.startsWith(`tel:`)||r.startsWith(`sms:`)||r.startsWith(`javascript:`)||r.startsWith(`data:`)))try{let r=new URL(t,n);if(r.origin!==n){e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`);continue}let i=`${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//,`/`).replace(/^\/openlink\//,`/`);e.setAttribute(`href`,i),e.classList.add(`internal-link`),e.removeAttribute(`target`),e.removeAttribute(`rel`)}catch{}}},o=e=>typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`,s=e=>{try{if(typeof window>`u`)return{handled:!1};let t=window?.staffbase?.plugin?.util?.openLink;if(typeof t!=`function`)return{handled:!1};let n=t(e,{}),r;return n&&o(n)&&(r=n),{handled:!0,promise:r}}catch{return{handled:!1}}},c=e=>{if(!e||!i(e))return!1;let t=s(e);if(t.handled){try{let n=typeof window<`u`?window.location.href:void 0,r=setTimeout(()=>{try{let t=typeof window<`u`?window.location.href:void 0,r=typeof document<`u`?document.visibilityState:void 0;n&&t&&n===t&&r===`visible`&&typeof window<`u`&&window.location.assign(e)}catch{}},400);try{t.promise?.then(()=>{clearTimeout(r)}).catch(()=>{})}catch{}}catch{}return!0}try{typeof window<`u`&&window.location.assign(e)}catch{}return!1};exports.getInAppOpenLinkTarget=t,exports.isAllowedIframeSrc=r,exports.isSafeNavigationHref=i,exports.normalizeInAppLinks=a,exports.openStaffbaseAware=c,exports.tryOpenWithStaffbase=s;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./openStaffbaseAware-CWYBlqcd.js");var t=[`youtube.com`,`youtube-nocookie.com`,`youtu.be`,`vimeo.com`,`player.vimeo.com`,`staffbase.com`,`staffbase.rocks`],n=e=>{let n=e.trim();if(!n)return!1;try{let e=typeof window<`u`?window.location.origin:`https://localhost`,r=new URL(n,e);if(r.protocol!==`https:`&&r.protocol!==`http:`)return!1;if(r.origin===e)return!0;let i=r.hostname.toLowerCase();return t.some(e=>i===e||i.endsWith(`.${e}`))}catch{return!1}};exports.getInAppOpenLinkTarget=e.a,exports.isAllowedIframeSrc=n,exports.isSafeNavigationHref=e.i,exports.normalizeInAppLinks=e.r,exports.openStaffbaseAware=e.t,exports.tryOpenWithStaffbase=e.n;
2
2
  //# sourceMappingURL=links.cjs.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"links.cjs.js","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"links.cjs.js","names":[],"sources":["../src/links/isAllowedIframeSrc.ts"],"sourcesContent":["// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n"],"mappings":"uHAKA,IAAM,EAA+B,CACnC,cACA,uBACA,WACA,YACA,mBACA,gBACA,iBACF,EASa,EAAsB,GAAyB,CAC1D,IAAM,EAAU,EAAI,KAAK,EACzB,GAAI,CAAC,EAAS,MAAO,GAErB,GAAI,CACF,IAAM,EACJ,OAAO,OAAW,IACd,OAAO,SAAS,OAChB,oBACA,EAAM,IAAI,IAAI,EAAS,CAAM,EAEnC,GAAI,EAAI,WAAa,UAAY,EAAI,WAAa,QAAS,MAAO,GAClE,GAAI,EAAI,SAAW,EAAQ,MAAO,GAElC,IAAM,EAAO,EAAI,SAAS,YAAY,EACtC,OAAO,EAA6B,KACjC,GAAW,IAAS,GAAU,EAAK,SAAS,IAAI,GAAQ,CAC3D,CACF,MAAQ,CACN,MAAO,EACT,CACF"}
package/dist/links.es.mjs CHANGED
@@ -1,20 +1,6 @@
1
- //#region src/links/stripStaffbaseLinkPrefix.ts
2
- var e = (e) => e.startsWith("/deeplink/") || e.startsWith("/openlink/") ? `/${e.slice(10)}` : e, t = (t, n) => {
3
- let r = t?.trim();
4
- if (!r || r.startsWith("#")) return null;
5
- let i = r.toLowerCase();
6
- if (i.startsWith("mailto:") || i.startsWith("tel:") || i.startsWith("sms:") || i.startsWith("javascript:") || i.startsWith("data:")) return null;
7
- try {
8
- let t = new URL(n), i = new URL(r, t.toString());
9
- if (i.origin === t.origin) {
10
- let n = e(i.pathname);
11
- return new URL(`${n}${i.search}${i.hash}`, t.origin).toString();
12
- }
13
- return i.toString();
14
- } catch {
15
- return null;
16
- }
17
- }, n = [
1
+ import { a as e, i as t, n, r, t as i } from "./openStaffbaseAware-CJs5uAvX.mjs";
2
+ //#region src/links/isAllowedIframeSrc.ts
3
+ var a = [
18
4
  "youtube.com",
19
5
  "youtube-nocookie.com",
20
6
  "youtu.be",
@@ -22,77 +8,20 @@ var e = (e) => e.startsWith("/deeplink/") || e.startsWith("/openlink/") ? `/${e.
22
8
  "player.vimeo.com",
23
9
  "staffbase.com",
24
10
  "staffbase.rocks"
25
- ], r = (e) => {
11
+ ], o = (e) => {
26
12
  let t = e.trim();
27
13
  if (!t) return !1;
28
14
  try {
29
- let e = typeof window < "u" ? window.location.origin : "https://localhost", r = new URL(t, e);
30
- if (r.protocol !== "https:" && r.protocol !== "http:") return !1;
31
- if (r.origin === e) return !0;
32
- let i = r.hostname.toLowerCase();
33
- return n.some((e) => i === e || i.endsWith(`.${e}`));
15
+ let e = typeof window < "u" ? window.location.origin : "https://localhost", n = new URL(t, e);
16
+ if (n.protocol !== "https:" && n.protocol !== "http:") return !1;
17
+ if (n.origin === e) return !0;
18
+ let r = n.hostname.toLowerCase();
19
+ return a.some((e) => r === e || r.endsWith(`.${e}`));
34
20
  } catch {
35
21
  return !1;
36
22
  }
37
- }, i = (e) => {
38
- let t = e.trim().toLowerCase();
39
- return t ? !(t.startsWith("javascript:") || t.startsWith("data:") || t.startsWith("vbscript:")) : !1;
40
- }, a = (e, t) => {
41
- let n = t?.trim() || "";
42
- if (!n) return;
43
- let r = Array.from(e.querySelectorAll("a[href]"));
44
- for (let e of r) {
45
- let t = e.getAttribute("href");
46
- if (!t) continue;
47
- let r = t.trim().toLowerCase();
48
- if (!(r.startsWith("#") || r.startsWith("mailto:") || r.startsWith("tel:") || r.startsWith("sms:") || r.startsWith("javascript:") || r.startsWith("data:"))) try {
49
- let r = new URL(t, n);
50
- if (r.origin !== n) {
51
- e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer");
52
- continue;
53
- }
54
- let i = `${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//, "/").replace(/^\/openlink\//, "/");
55
- e.setAttribute("href", i), e.classList.add("internal-link"), e.removeAttribute("target"), e.removeAttribute("rel");
56
- } catch {}
57
- }
58
- }, o = (e) => typeof e == "object" && !!e && "then" in e && typeof e.then == "function", s = (e) => {
59
- try {
60
- if (typeof window > "u") return { handled: !1 };
61
- let t = window?.staffbase?.plugin?.util?.openLink;
62
- if (typeof t != "function") return { handled: !1 };
63
- let n = t(e, {}), r;
64
- return n && o(n) && (r = n), {
65
- handled: !0,
66
- promise: r
67
- };
68
- } catch {
69
- return { handled: !1 };
70
- }
71
- }, c = (e) => {
72
- if (!e || !i(e)) return !1;
73
- let t = s(e);
74
- if (t.handled) {
75
- try {
76
- let n = typeof window < "u" ? window.location.href : void 0, r = setTimeout(() => {
77
- try {
78
- let t = typeof window < "u" ? window.location.href : void 0, r = typeof document < "u" ? document.visibilityState : void 0;
79
- n && t && n === t && r === "visible" && typeof window < "u" && window.location.assign(e);
80
- } catch {}
81
- }, 400);
82
- try {
83
- t.promise?.then(() => {
84
- clearTimeout(r);
85
- }).catch(() => {});
86
- } catch {}
87
- } catch {}
88
- return !0;
89
- }
90
- try {
91
- typeof window < "u" && window.location.assign(e);
92
- } catch {}
93
- return !1;
94
23
  };
95
24
  //#endregion
96
- export { t as getInAppOpenLinkTarget, r as isAllowedIframeSrc, i as isSafeNavigationHref, a as normalizeInAppLinks, c as openStaffbaseAware, s as tryOpenWithStaffbase };
25
+ export { e as getInAppOpenLinkTarget, o as isAllowedIframeSrc, t as isSafeNavigationHref, r as normalizeInAppLinks, i as openStaffbaseAware, n as tryOpenWithStaffbase };
97
26
 
98
27
  //# sourceMappingURL=links.es.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"links.es.mjs","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isAllowedIframeSrc.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"links.es.mjs","names":[],"sources":["../src/links/isAllowedIframeSrc.ts"],"sourcesContent":["// Hosts allowed to be framed inside sanitized article content. Article HTML is\n// first-party authored in Staffbase, but it renders inside a session-bearing\n// webview, so iframe sources are constrained to known embed providers plus the\n// Staffbase origin rather than allowing arbitrary framing. Promoted from\n// staffbase-alerts.\nconst ALLOWED_IFRAME_HOST_SUFFIXES = [\n 'youtube.com',\n 'youtube-nocookie.com',\n 'youtu.be',\n 'vimeo.com',\n 'player.vimeo.com',\n 'staffbase.com',\n 'staffbase.rocks',\n]\n\n/**\n * Returns true when an iframe src may be rendered. Same-origin sources are\n * always allowed; cross-origin sources must match the embed allowlist. Pair with\n * sanitizeArticleHtml's `isAllowedIframeSrc` option.\n * @param {string} src - The iframe src attribute value.\n * @returns {boolean} True when the iframe may be kept.\n */\nexport const isAllowedIframeSrc = (src: string): boolean => {\n const trimmed = src.trim()\n if (!trimmed) return false\n\n try {\n const origin =\n typeof window !== 'undefined'\n ? window.location.origin\n : 'https://localhost'\n const url = new URL(trimmed, origin)\n\n if (url.protocol !== 'https:' && url.protocol !== 'http:') return false\n if (url.origin === origin) return true\n\n const host = url.hostname.toLowerCase()\n return ALLOWED_IFRAME_HOST_SUFFIXES.some(\n (suffix) => host === suffix || host.endsWith(`.${suffix}`),\n )\n } catch {\n return false\n }\n}\n"],"mappings":";;AAKA,IAAM,IAA+B;CACnC;CACA;CACA;CACA;CACA;CACA;CACA;AACF,GASa,KAAsB,MAAyB;CAC1D,IAAM,IAAU,EAAI,KAAK;CACzB,IAAI,CAAC,GAAS,OAAO;CAErB,IAAI;EACF,IAAM,IACJ,OAAO,SAAW,MACd,OAAO,SAAS,SAChB,qBACA,IAAM,IAAI,IAAI,GAAS,CAAM;EAEnC,IAAI,EAAI,aAAa,YAAY,EAAI,aAAa,SAAS,OAAO;EAClE,IAAI,EAAI,WAAW,GAAQ,OAAO;EAElC,IAAM,IAAO,EAAI,SAAS,YAAY;EACtC,OAAO,EAA6B,MACjC,MAAW,MAAS,KAAU,EAAK,SAAS,IAAI,GAAQ,CAC3D;CACF,QAAQ;EACN,OAAO;CACT;AACF"}
@@ -0,0 +1,78 @@
1
+ //#region src/links/stripStaffbaseLinkPrefix.ts
2
+ var e = (e) => e.startsWith("/deeplink/") || e.startsWith("/openlink/") ? `/${e.slice(10)}` : e, t = (t, n) => {
3
+ let r = t?.trim();
4
+ if (!r || r.startsWith("#")) return null;
5
+ let i = r.toLowerCase();
6
+ if (i.startsWith("mailto:") || i.startsWith("tel:") || i.startsWith("sms:") || i.startsWith("javascript:") || i.startsWith("data:")) return null;
7
+ try {
8
+ let t = new URL(n), i = new URL(r, t.toString());
9
+ if (i.origin === t.origin) {
10
+ let n = e(i.pathname);
11
+ return new URL(`${n}${i.search}${i.hash}`, t.origin).toString();
12
+ }
13
+ return i.toString();
14
+ } catch {
15
+ return null;
16
+ }
17
+ }, n = (e) => {
18
+ let t = e.trim().toLowerCase();
19
+ return t ? !(t.startsWith("javascript:") || t.startsWith("data:") || t.startsWith("vbscript:")) : !1;
20
+ }, r = (e, t) => {
21
+ let n = t?.trim() || "";
22
+ if (!n) return;
23
+ let r = Array.from(e.querySelectorAll("a[href]"));
24
+ for (let e of r) {
25
+ let t = e.getAttribute("href");
26
+ if (!t) continue;
27
+ let r = t.trim().toLowerCase();
28
+ if (!(r.startsWith("#") || r.startsWith("mailto:") || r.startsWith("tel:") || r.startsWith("sms:") || r.startsWith("javascript:") || r.startsWith("data:"))) try {
29
+ let r = new URL(t, n);
30
+ if (r.origin !== n) {
31
+ e.getAttribute("target") === "_blank" && e.setAttribute("rel", "noopener noreferrer");
32
+ continue;
33
+ }
34
+ let i = `${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//, "/").replace(/^\/openlink\//, "/");
35
+ e.setAttribute("href", i), e.classList.add("internal-link"), e.removeAttribute("target"), e.removeAttribute("rel");
36
+ } catch {}
37
+ }
38
+ }, i = (e) => typeof e == "object" && !!e && "then" in e && typeof e.then == "function", a = (e) => {
39
+ try {
40
+ if (typeof window > "u") return { handled: !1 };
41
+ let t = window?.staffbase?.plugin?.util?.openLink;
42
+ if (typeof t != "function") return { handled: !1 };
43
+ let n = t(e, {}), r;
44
+ return n && i(n) && (r = n), {
45
+ handled: !0,
46
+ promise: r
47
+ };
48
+ } catch {
49
+ return { handled: !1 };
50
+ }
51
+ }, o = (e) => {
52
+ if (!e || !n(e)) return !1;
53
+ let t = a(e);
54
+ if (t.handled) {
55
+ try {
56
+ let n = typeof window < "u" ? window.location.href : void 0, r = setTimeout(() => {
57
+ try {
58
+ let t = typeof window < "u" ? window.location.href : void 0, r = typeof document < "u" ? document.visibilityState : void 0;
59
+ n && t && n === t && r === "visible" && typeof window < "u" && window.location.assign(e);
60
+ } catch {}
61
+ }, 400);
62
+ try {
63
+ t.promise?.then(() => {
64
+ clearTimeout(r);
65
+ }).catch(() => {});
66
+ } catch {}
67
+ } catch {}
68
+ return !0;
69
+ }
70
+ try {
71
+ typeof window < "u" && window.location.assign(e);
72
+ } catch {}
73
+ return !1;
74
+ };
75
+ //#endregion
76
+ export { t as a, n as i, a as n, r, o as t };
77
+
78
+ //# sourceMappingURL=openStaffbaseAware-CJs5uAvX.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openStaffbaseAware-CJs5uAvX.mjs","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ var e=e=>e.startsWith(`/deeplink/`)||e.startsWith(`/openlink/`)?`/${e.slice(10)}`:e,t=(t,n)=>{let r=t?.trim();if(!r||r.startsWith(`#`))return null;let i=r.toLowerCase();if(i.startsWith(`mailto:`)||i.startsWith(`tel:`)||i.startsWith(`sms:`)||i.startsWith(`javascript:`)||i.startsWith(`data:`))return null;try{let t=new URL(n),i=new URL(r,t.toString());if(i.origin===t.origin){let n=e(i.pathname);return new URL(`${n}${i.search}${i.hash}`,t.origin).toString()}return i.toString()}catch{return null}},n=e=>{let t=e.trim().toLowerCase();return t?!(t.startsWith(`javascript:`)||t.startsWith(`data:`)||t.startsWith(`vbscript:`)):!1},r=(e,t)=>{let n=t?.trim()||``;if(!n)return;let r=Array.from(e.querySelectorAll(`a[href]`));for(let e of r){let t=e.getAttribute(`href`);if(!t)continue;let r=t.trim().toLowerCase();if(!(r.startsWith(`#`)||r.startsWith(`mailto:`)||r.startsWith(`tel:`)||r.startsWith(`sms:`)||r.startsWith(`javascript:`)||r.startsWith(`data:`)))try{let r=new URL(t,n);if(r.origin!==n){e.getAttribute(`target`)===`_blank`&&e.setAttribute(`rel`,`noopener noreferrer`);continue}let i=`${r.pathname}${r.search}${r.hash}`.replace(/^\/deeplink\//,`/`).replace(/^\/openlink\//,`/`);e.setAttribute(`href`,i),e.classList.add(`internal-link`),e.removeAttribute(`target`),e.removeAttribute(`rel`)}catch{}}},i=e=>typeof e==`object`&&!!e&&`then`in e&&typeof e.then==`function`,a=e=>{try{if(typeof window>`u`)return{handled:!1};let t=window?.staffbase?.plugin?.util?.openLink;if(typeof t!=`function`)return{handled:!1};let n=t(e,{}),r;return n&&i(n)&&(r=n),{handled:!0,promise:r}}catch{return{handled:!1}}},o=e=>{if(!e||!n(e))return!1;let t=a(e);if(t.handled){try{let n=typeof window<`u`?window.location.href:void 0,r=setTimeout(()=>{try{let t=typeof window<`u`?window.location.href:void 0,r=typeof document<`u`?document.visibilityState:void 0;n&&t&&n===t&&r===`visible`&&typeof window<`u`&&window.location.assign(e)}catch{}},400);try{t.promise?.then(()=>{clearTimeout(r)}).catch(()=>{})}catch{}}catch{}return!0}try{typeof window<`u`&&window.location.assign(e)}catch{}return!1};Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return t}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return a}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return o}});
2
+ //# sourceMappingURL=openStaffbaseAware-CWYBlqcd.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openStaffbaseAware-CWYBlqcd.js","names":[],"sources":["../src/links/stripStaffbaseLinkPrefix.ts","../src/links/getInAppOpenLinkTarget.ts","../src/links/isSafeNavigationHref.ts","../src/links/normalizeInAppLinks.ts","../src/links/isPromiseLike.ts","../src/links/tryOpenWithStaffbase.ts","../src/links/openStaffbaseAware.ts"],"sourcesContent":["/**\n * Strips a leading Staffbase link prefix (`/deeplink/` or `/openlink/`) from a\n * URL pathname, leaving the canonical in-app path.\n * @param {string} path - The URL pathname.\n * @returns {string} The path without the Staffbase prefix.\n */\nexport const stripStaffbaseLinkPrefix = (path: string): string => {\n if (path.startsWith('/deeplink/'))\n return `/${path.slice('/deeplink/'.length)}`\n if (path.startsWith('/openlink/'))\n return `/${path.slice('/openlink/'.length)}`\n return path\n}\n","import { stripStaffbaseLinkPrefix } from './stripStaffbaseLinkPrefix'\n\n/**\n * Returns an absolute URL string that is safe to pass into Staffbase's\n * `openLink()`. We intentionally do NOT require `/openlink/`; if the platform\n * strips it, links can still open in-app by delegating to `openLink()`. Ported\n * from staffbase-alerts (identical to unack).\n * @param {string | null | undefined} href - Raw href as found on an <a> element.\n * @param {string} baseUrl - Base URL used to resolve relative hrefs (usually window.location.origin).\n * @returns {string | null} An absolute URL string, or null if it should not be handled.\n */\nexport const getInAppOpenLinkTarget = (\n href: string | null | undefined,\n baseUrl: string,\n): string | null => {\n const trimmed = href?.trim()\n if (!trimmed) return null\n\n // Ignore anchors and non-navigational links.\n if (trimmed.startsWith('#')) return null\n\n // Let the browser handle special protocols.\n const lower = trimmed.toLowerCase()\n if (\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n return null\n }\n\n try {\n const base = new URL(baseUrl)\n const url = new URL(trimmed, base.toString())\n\n // Canonicalize same-origin links by stripping Staffbase prefixes, but keep them absolute.\n if (url.origin === base.origin) {\n const cleanedPath = stripStaffbaseLinkPrefix(url.pathname)\n const absolute = new URL(\n `${cleanedPath}${url.search}${url.hash}`,\n base.origin,\n )\n return absolute.toString()\n }\n\n // For cross-origin absolute URLs, return as-is.\n return url.toString()\n } catch {\n return null\n }\n}\n","/**\n * Returns true when an href is safe to pass to a real browser navigation\n * (e.g. `window.location.assign`). Blocks scripted/inline schemes that could\n * execute in the session-bearing webview. Relative URLs and http(s)/mailto/tel\n * are allowed. Promoted from staffbase-alerts.\n * @param {string} href - The href to validate.\n * @returns {boolean} True when the href is safe to navigate to.\n */\nexport const isSafeNavigationHref = (href: string): boolean => {\n const trimmed = href.trim().toLowerCase()\n if (!trimmed) return false\n\n return !(\n trimmed.startsWith('javascript:') ||\n trimmed.startsWith('data:') ||\n trimmed.startsWith('vbscript:')\n )\n}\n","/**\n * Normalizes links inside Staffbase-rendered HTML so they behave better in mobile\n * apps. On iOS, absolute same-origin links and/or `target=\"_blank\"` can trigger\n * Safari instead of in-app navigation; for same-origin links we rewrite to\n * relative paths and remove target/rel. Ported from staffbase-alerts (the\n * superset): cross-origin `target=\"_blank\"` links are hardened with\n * `rel=\"noopener noreferrer\"` instead of left untouched. The Staffbase origin is\n * passed in (the lib never reads env).\n * @param {HTMLElement} root - The container whose anchors are normalized.\n * @param {string} staffbaseOrigin - The Staffbase origin used to detect same-origin links.\n * @returns {void}\n */\nexport const normalizeInAppLinks = (\n root: HTMLElement,\n staffbaseOrigin: string,\n): void => {\n const origin = staffbaseOrigin?.trim() || ''\n if (!origin) return\n\n const anchors = Array.from(root.querySelectorAll('a[href]'))\n\n for (const anchor of anchors) {\n const rawHref = anchor.getAttribute('href')\n if (!rawHref) continue\n\n // Skip special protocols and anchors.\n const lower = rawHref.trim().toLowerCase()\n if (\n lower.startsWith('#') ||\n lower.startsWith('mailto:') ||\n lower.startsWith('tel:') ||\n lower.startsWith('sms:') ||\n lower.startsWith('javascript:') ||\n lower.startsWith('data:')\n ) {\n continue\n }\n\n try {\n const url = new URL(rawHref, origin)\n\n // Only rewrite links that point to the same Staffbase origin.\n if (url.origin !== origin) {\n // Harden cross-origin new-tab links against reverse tabnabbing rather\n // than leaving the authored target/rel untouched.\n if (anchor.getAttribute('target') === '_blank') {\n anchor.setAttribute('rel', 'noopener noreferrer')\n }\n continue\n }\n\n // Convert to a relative URL so the mobile app treats it as in-app navigation.\n const relative = `${url.pathname}${url.search}${url.hash}`\n const withoutStaffbasePrefix = relative\n .replace(/^\\/deeplink\\//, '/')\n .replace(/^\\/openlink\\//, '/')\n anchor.setAttribute('href', withoutStaffbasePrefix)\n\n // Staffbase uses this class in various contexts to mark links as internal.\n // Adding it here increases the chance that the iOS app routes the navigation in-app.\n anchor.classList.add('internal-link')\n\n // Avoid iOS opening Safari for \"new window\" navigation.\n anchor.removeAttribute('target')\n anchor.removeAttribute('rel')\n } catch {\n // If parsing fails, leave link as-is.\n }\n }\n}\n","/**\n * Type guard for Promise-like (thenable) values, used to normalize whatever\n * Staffbase's openLink returns.\n * @param {unknown} value - The value to test.\n * @returns {boolean} True when value has a callable then method.\n */\nexport const isPromiseLike = (value: unknown): value is PromiseLike<unknown> =>\n value !== null &&\n typeof value === 'object' &&\n 'then' in value &&\n typeof (value as PromiseLike<unknown>).then === 'function'\n","import type { TryOpenResult } from '../types/links/TryOpenResult'\nimport type { WindowWithStaffbase } from '../types/links/WindowWithStaffbase'\n\nimport { isPromiseLike } from './isPromiseLike'\n\n/**\n * Attempt to open a link using Staffbase's plugin util.openLink if available.\n * Ported from staffbase-alerts (typed Promise-like guard; smart-search used\n * `as any`).\n * @param {string} href - The target URL to open.\n * @returns {TryOpenResult} Whether Staffbase handled it and a promise if available.\n */\nexport const tryOpenWithStaffbase = (href: string): TryOpenResult => {\n try {\n if (typeof window === 'undefined') {\n return { handled: false }\n }\n const w = window as WindowWithStaffbase\n const open = w?.staffbase?.plugin?.util?.openLink\n const hasOpenLink = typeof open === 'function'\n if (!hasOpenLink) return { handled: false }\n // Pass empty options as the second parameter to align with openLink signature\n const maybePromise = open(href, {})\n // If a Promise is returned, normalize it\n let normalizedPromise: Promise<void> | undefined\n if (maybePromise && isPromiseLike(maybePromise)) {\n normalizedPromise = maybePromise as Promise<void>\n }\n return { handled: true, promise: normalizedPromise }\n } catch {\n return { handled: false }\n }\n}\n","import { isSafeNavigationHref } from './isSafeNavigationHref'\nimport { tryOpenWithStaffbase } from './tryOpenWithStaffbase'\n\n/**\n * Try to open with Staffbase; if not available, navigate normally in the same\n * tab. Ported from staffbase-alerts (the superset): it guards with\n * isSafeNavigationHref first, so scripted schemes never reach navigation.\n * @param {string} href - The target URL to open.\n * @returns {boolean} True if Staffbase handled the navigation, otherwise false.\n */\nexport const openStaffbaseAware = (href: string): boolean => {\n if (!href) return false\n\n // Never navigate to scripted/inline schemes (javascript:, data:, vbscript:),\n // which would execute in the session-bearing webview.\n if (!isSafeNavigationHref(href)) return false\n\n const result = tryOpenWithStaffbase(href)\n if (result.handled) {\n // Watchdog: if Staffbase openLink is a no-op, force navigation shortly after\n try {\n const beforeHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const timer = setTimeout(() => {\n try {\n const afterHref =\n typeof window !== 'undefined' ? window.location.href : undefined\n const visibility =\n typeof document !== 'undefined'\n ? document.visibilityState\n : undefined\n const noChange = beforeHref && afterHref && beforeHref === afterHref\n const stillVisible = visibility === 'visible'\n if (noChange && stillVisible && typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore watchdog navigation failures.\n }\n }, 400)\n // If the Staffbase promise resolves, cancel the watchdog fallback\n try {\n result.promise\n ?.then(() => {\n clearTimeout(timer)\n })\n .catch(() => {\n // Keep the watchdog active on rejection.\n })\n } catch {\n // Ignore promise-wiring failures.\n }\n } catch {\n // Ignore watchdog setup failures.\n }\n return true\n }\n try {\n if (typeof window !== 'undefined') {\n window.location.assign(href)\n }\n } catch {\n // Ignore navigation failures.\n }\n return false\n}\n"],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Whether a native link event exposes MouseEvent modifier properties.
4
+ * @param {NativeLinkEvent} e - The native event.
5
+ * @returns {boolean} True when the event has `button` and `metaKey`.
6
+ */
7
+ export declare const hasMouseEventProperties: (e: NativeLinkEvent) => e is MouseEvent | PointerEvent;
8
+ //# sourceMappingURL=hasMouseEventProperties.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hasMouseEventProperties.d.ts","sourceRoot":"","sources":["../../../../src/links/react/hasMouseEventProperties.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAExE;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,GAClC,GAAG,eAAe,KACjB,CAAC,IAAI,UAAU,GAAG,YAEpB,CAAA"}
@@ -0,0 +1,4 @@
1
+ export { useInAppLinkHandling } from './useInAppLinkHandling';
2
+ export type { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions';
3
+ export type { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/links/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,YAAY,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAA;AAChG,YAAY,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Whether a native link event is "modified" and should be left to the browser
4
+ * (default prevented, a non-primary mouse button, or a modifier key held).
5
+ * @param {NativeLinkEvent} e - The native event.
6
+ * @returns {boolean} True when the event should not be intercepted.
7
+ */
8
+ export declare const isModifiedNativeEvent: (e: NativeLinkEvent) => boolean;
9
+ //# sourceMappingURL=isModifiedNativeEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isModifiedNativeEvent.d.ts","sourceRoot":"","sources":["../../../../src/links/react/isModifiedNativeEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAGxE;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,GAAG,eAAe,KAAG,OAgB1D,CAAA"}
@@ -0,0 +1,12 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Whether a value is a real native DOM event (as opposed to a React synthetic
4
+ * event-like object), so the Staffbase content open-link path can be tried.
5
+ * @param {NativeLinkEvent | { preventDefault?: () => void; stopPropagation?: () => void } | undefined} e - The candidate.
6
+ * @returns {boolean} True when the value is a native Event.
7
+ */
8
+ export declare const isNativeLinkEvent: (e: {
9
+ preventDefault?: () => void;
10
+ stopPropagation?: () => void;
11
+ } | NativeLinkEvent | undefined) => e is NativeLinkEvent;
12
+ //# sourceMappingURL=isNativeLinkEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isNativeLinkEvent.d.ts","sourceRoot":"","sources":["../../../../src/links/react/isNativeLinkEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAExE;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,GAC5B,GACI;IAAE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,IAAI,CAAA;CAAE,GAC7D,eAAe,GACf,SAAS,KACZ,CAAC,IAAI,eAEP,CAAA"}
@@ -0,0 +1,8 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Whether a native link event is a TouchEvent (has `changedTouches`).
4
+ * @param {NativeLinkEvent} e - The native event.
5
+ * @returns {boolean} True when the event is a TouchEvent.
6
+ */
7
+ export declare const isTouchLinkEvent: (e: NativeLinkEvent) => e is TouchEvent;
8
+ //# sourceMappingURL=isTouchLinkEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isTouchLinkEvent.d.ts","sourceRoot":"","sources":["../../../../src/links/react/isTouchLinkEvent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAExE;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,GAAI,GAAG,eAAe,KAAG,CAAC,IAAI,UAE1D,CAAA"}
@@ -0,0 +1,7 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Tracks the capture-phase handler installed per container element, so the same
4
+ * container is never wired twice and can be cleanly torn down on unmount.
5
+ */
6
+ export declare const nativeLinkHandlers: WeakMap<HTMLElement, (e: NativeLinkEvent) => void>;
7
+ //# sourceMappingURL=nativeLinkHandlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nativeLinkHandlers.d.ts","sourceRoot":"","sources":["../../../../src/links/react/nativeLinkHandlers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAExE;;;GAGG;AACH,eAAO,MAAM,kBAAkB,2BAEzB,eAAe,KAAK,IAAI,CAC3B,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { NativeLinkEvent } from '../../types/links/NativeLinkEvent';
2
+ /**
3
+ * Tries the host's own content open-link helper (the one Staffbase's widget
4
+ * manager uses), to stay maximally aligned with platform behavior. Returns false
5
+ * when the helper is absent or throws, so the caller can fall back.
6
+ * @param {NativeLinkEvent} e - The originating native event.
7
+ * @returns {boolean} True when the host helper handled the open.
8
+ */
9
+ export declare const tryStaffbaseContentOpenLink: (e: NativeLinkEvent) => boolean;
10
+ //# sourceMappingURL=tryStaffbaseContentOpenLink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tryStaffbaseContentOpenLink.d.ts","sourceRoot":"","sources":["../../../../src/links/react/tryStaffbaseContentOpenLink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAA;AAGxE;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GAAI,GAAG,eAAe,KAAG,OAgBhE,CAAA"}
@@ -0,0 +1,15 @@
1
+ import { UseInAppLinkHandlingOptions } from '../../types/links/UseInAppLinkHandlingOptions';
2
+ import { UseInAppLinkHandlingReturn } from '../../types/links/UseInAppLinkHandlingReturn';
3
+ /**
4
+ * Centralizes in-content link handling for Staffbase mobile/webview environments.
5
+ *
6
+ * Always attempts Staffbase-aware navigation (the host openLink when available,
7
+ * otherwise same-tab navigation). It exposes React handlers for click/touchend/
8
+ * pointerup and, when `containerRef` is given, installs a more reliable native
9
+ * capture-phase handler. Touch/pointer/click are de-duplicated within a short
10
+ * window so a single tap never fires navigation twice in iOS webviews.
11
+ * @param {UseInAppLinkHandlingOptions} options - Origin, after-open callback and optional container ref.
12
+ * @returns {UseInAppLinkHandlingReturn} prepareHtmlContent plus the React event handlers.
13
+ */
14
+ export declare const useInAppLinkHandling: ({ staffbaseOrigin, onAfterOpen, containerRef, }: UseInAppLinkHandlingOptions) => UseInAppLinkHandlingReturn;
15
+ //# sourceMappingURL=useInAppLinkHandling.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useInAppLinkHandling.d.ts","sourceRoot":"","sources":["../../../../src/links/react/useInAppLinkHandling.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,+CAA+C,CAAA;AAChG,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,8CAA8C,CAAA;AAY9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAI,iDAIlC,2BAA2B,KAAG,0BAwLhC,CAAA"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Union of the native DOM events the in-app link handler intercepts on a
3
+ * content container (click, touchend, pointerup).
4
+ */
5
+ export type NativeLinkEvent = MouseEvent | TouchEvent | PointerEvent;
6
+ //# sourceMappingURL=NativeLinkEvent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeLinkEvent.d.ts","sourceRoot":"","sources":["../../../../src/types/links/NativeLinkEvent.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,UAAU,GAAG,YAAY,CAAA"}
@@ -0,0 +1,35 @@
1
+ import { NativeLinkEvent } from './NativeLinkEvent';
2
+ /**
3
+ * Window augmented with the host's content link-open surface
4
+ * (`window.staffbase.content.*.openLink`), the helper Staffbase's own widget
5
+ * manager uses for in-content links. Every level is optional because the host
6
+ * API is undocumented and may be absent.
7
+ */
8
+ export interface StaffbaseContentWindow {
9
+ staffbase?: {
10
+ content?: {
11
+ link?: {
12
+ openLink?: ContentLinkOpener;
13
+ };
14
+ links?: {
15
+ openLink?: ContentLinkOpener;
16
+ };
17
+ loader?: {
18
+ link?: {
19
+ openLink?: ContentLinkOpener;
20
+ };
21
+ };
22
+ };
23
+ };
24
+ }
25
+ /**
26
+ * The host's content link opener signature.
27
+ * @param {{ useDefault: boolean }} options - Whether to fall back to default handling.
28
+ * @param {NativeLinkEvent} event - The originating native event.
29
+ * @returns {void} Nothing.
30
+ */
31
+ type ContentLinkOpener = (options: {
32
+ useDefault: boolean;
33
+ }, event: NativeLinkEvent) => void;
34
+ export {};
35
+ //# sourceMappingURL=StaffbaseContentWindow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaffbaseContentWindow.d.ts","sourceRoot":"","sources":["../../../../src/types/links/StaffbaseContentWindow.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD;;;;;GAKG;AACH,MAAM,WAAW,sBAAsB;IACrC,SAAS,CAAC,EAAE;QACV,OAAO,CAAC,EAAE;YACR,IAAI,CAAC,EAAE;gBAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;aAAE,CAAA;YACvC,KAAK,CAAC,EAAE;gBAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;aAAE,CAAA;YACxC,MAAM,CAAC,EAAE;gBAAE,IAAI,CAAC,EAAE;oBAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAA;iBAAE,CAAA;aAAE,CAAA;SACrD,CAAA;KACF,CAAA;CACF;AAED;;;;;GAKG;AACH,KAAK,iBAAiB,GAAG,CACvB,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,CAAA;CAAE,EAChC,KAAK,EAAE,eAAe,KACnB,IAAI,CAAA"}
@@ -0,0 +1,19 @@
1
+ import { RefObject } from 'react';
2
+ /**
3
+ * Options for the useInAppLinkHandling hook.
4
+ */
5
+ export interface UseInAppLinkHandlingOptions {
6
+ /** Staffbase origin used to rewrite/resolve in-app links (injected, no env). */
7
+ staffbaseOrigin: string;
8
+ /**
9
+ * Called after navigation starts for an intercepted link (e.g. close a drawer).
10
+ * @returns {void} Nothing.
11
+ */
12
+ onAfterOpen?: () => void;
13
+ /**
14
+ * When provided, installs a native capture-phase handler on the container,
15
+ * which is more reliable than React synthetic events in some iOS app webviews.
16
+ */
17
+ containerRef?: RefObject<HTMLElement | null>;
18
+ }
19
+ //# sourceMappingURL=UseInAppLinkHandlingOptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UseInAppLinkHandlingOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/links/UseInAppLinkHandlingOptions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAA;AAEtC;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAA;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IACxB;;;OAGG;IACH,YAAY,CAAC,EAAE,SAAS,CAAC,WAAW,GAAG,IAAI,CAAC,CAAA;CAC7C"}
@@ -0,0 +1,31 @@
1
+ import { MouseEvent, PointerEvent, TouchEvent } from 'react';
2
+ /**
3
+ * Handlers and helpers returned by useInAppLinkHandling.
4
+ */
5
+ export interface UseInAppLinkHandlingReturn {
6
+ /**
7
+ * Rewrites in-app links inside the element for iOS-friendly navigation.
8
+ * @param {HTMLElement} content - The content container to mutate.
9
+ * @returns {HTMLElement} The same element, after rewriting.
10
+ */
11
+ prepareHtmlContent: (content: HTMLElement) => HTMLElement;
12
+ /**
13
+ * React click handler that routes in-content links through Staffbase-aware navigation.
14
+ * @param {MouseEvent<HTMLElement>} e - The React mouse event.
15
+ * @returns {void} Nothing.
16
+ */
17
+ handleContentClick: (e: MouseEvent<HTMLElement>) => void;
18
+ /**
19
+ * React touchend handler (more reliable than click in some webviews).
20
+ * @param {TouchEvent<HTMLElement>} e - The React touch event.
21
+ * @returns {void} Nothing.
22
+ */
23
+ handleContentTouchEnd: (e: TouchEvent<HTMLElement>) => void;
24
+ /**
25
+ * React pointerup handler (used when pointer events are enabled).
26
+ * @param {PointerEvent<HTMLElement>} e - The React pointer event.
27
+ * @returns {void} Nothing.
28
+ */
29
+ handleContentPointerUp: (e: PointerEvent<HTMLElement>) => void;
30
+ }
31
+ //# sourceMappingURL=UseInAppLinkHandlingReturn.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UseInAppLinkHandlingReturn.d.ts","sourceRoot":"","sources":["../../../../src/types/links/UseInAppLinkHandlingReturn.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,OAAO,CAAA;AAEjE;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,OAAO,EAAE,WAAW,KAAK,WAAW,CAAA;IACzD;;;;OAIG;IACH,kBAAkB,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;IACxD;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;IAC3D;;;;OAIG;IACH,sBAAsB,EAAE,CAAC,CAAC,EAAE,YAAY,CAAC,WAAW,CAAC,KAAK,IAAI,CAAA;CAC/D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@favish/staffbase-utils",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Shared internal/host utilities for Staffbase widgets",
5
5
  "author": "Favish <dev@favish.com>",
6
6
  "license": "UNLICENSED",
@@ -48,6 +48,11 @@
48
48
  "import": "./dist/widgets.es.mjs",
49
49
  "require": "./dist/widgets.cjs.js"
50
50
  },
51
+ "./links/react": {
52
+ "types": "./dist/src/links/react/index.d.ts",
53
+ "import": "./dist/links/react.es.mjs",
54
+ "require": "./dist/links/react.cjs.js"
55
+ },
51
56
  "./widgets/react": {
52
57
  "types": "./dist/src/widgets/react/index.d.ts",
53
58
  "import": "./dist/widgets/react.es.mjs",