@favish/staffbase-utils 0.10.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.
- package/README.md +3 -1
- package/dist/content.cjs.js +2 -0
- package/dist/content.cjs.js.map +1 -0
- package/dist/content.es.mjs +26 -0
- package/dist/content.es.mjs.map +1 -0
- package/dist/links/react.cjs.js +2 -0
- package/dist/links/react.cjs.js.map +1 -0
- package/dist/links/react.es.mjs +75 -0
- package/dist/links/react.es.mjs.map +1 -0
- package/dist/links.cjs.js +1 -1
- package/dist/links.cjs.js.map +1 -1
- package/dist/links.es.mjs +10 -81
- package/dist/links.es.mjs.map +1 -1
- package/dist/openStaffbaseAware-CJs5uAvX.mjs +78 -0
- package/dist/openStaffbaseAware-CJs5uAvX.mjs.map +1 -0
- package/dist/openStaffbaseAware-CWYBlqcd.js +2 -0
- package/dist/openStaffbaseAware-CWYBlqcd.js.map +1 -0
- package/dist/src/content/detectEditorLanguage.d.ts +8 -0
- package/dist/src/content/detectEditorLanguage.d.ts.map +1 -0
- package/dist/src/content/detectPreviewLanguage.d.ts +8 -0
- package/dist/src/content/detectPreviewLanguage.d.ts.map +1 -0
- package/dist/src/content/index.d.ts +6 -0
- package/dist/src/content/index.d.ts.map +1 -0
- package/dist/src/content/resolveActiveLanguage.d.ts +13 -0
- package/dist/src/content/resolveActiveLanguage.d.ts.map +1 -0
- package/dist/src/content/resolveLocalizedContent.d.ts +15 -0
- package/dist/src/content/resolveLocalizedContent.d.ts.map +1 -0
- package/dist/src/links/react/hasMouseEventProperties.d.ts +8 -0
- package/dist/src/links/react/hasMouseEventProperties.d.ts.map +1 -0
- package/dist/src/links/react/index.d.ts +4 -0
- package/dist/src/links/react/index.d.ts.map +1 -0
- package/dist/src/links/react/isModifiedNativeEvent.d.ts +9 -0
- package/dist/src/links/react/isModifiedNativeEvent.d.ts.map +1 -0
- package/dist/src/links/react/isNativeLinkEvent.d.ts +12 -0
- package/dist/src/links/react/isNativeLinkEvent.d.ts.map +1 -0
- package/dist/src/links/react/isTouchLinkEvent.d.ts +8 -0
- package/dist/src/links/react/isTouchLinkEvent.d.ts.map +1 -0
- package/dist/src/links/react/nativeLinkHandlers.d.ts +7 -0
- package/dist/src/links/react/nativeLinkHandlers.d.ts.map +1 -0
- package/dist/src/links/react/tryStaffbaseContentOpenLink.d.ts +10 -0
- package/dist/src/links/react/tryStaffbaseContentOpenLink.d.ts.map +1 -0
- package/dist/src/links/react/useInAppLinkHandling.d.ts +15 -0
- package/dist/src/links/react/useInAppLinkHandling.d.ts.map +1 -0
- package/dist/src/types/content/ArticleImage.d.ts +17 -0
- package/dist/src/types/content/ArticleImage.d.ts.map +1 -0
- package/dist/src/types/content/ArticleImageVariant.d.ts +13 -0
- package/dist/src/types/content/ArticleImageVariant.d.ts.map +1 -0
- package/dist/src/types/content/LocalizedContent.d.ts +16 -0
- package/dist/src/types/content/LocalizedContent.d.ts.map +1 -0
- package/dist/src/types/content/ResolveLocalizedContentOptions.d.ts +24 -0
- package/dist/src/types/content/ResolveLocalizedContentOptions.d.ts.map +1 -0
- package/dist/src/types/content/index.d.ts +3 -0
- package/dist/src/types/content/index.d.ts.map +1 -1
- package/dist/src/types/links/NativeLinkEvent.d.ts +6 -0
- package/dist/src/types/links/NativeLinkEvent.d.ts.map +1 -0
- package/dist/src/types/links/StaffbaseContentWindow.d.ts +35 -0
- package/dist/src/types/links/StaffbaseContentWindow.d.ts.map +1 -0
- package/dist/src/types/links/UseInAppLinkHandlingOptions.d.ts +19 -0
- package/dist/src/types/links/UseInAppLinkHandlingOptions.d.ts.map +1 -0
- package/dist/src/types/links/UseInAppLinkHandlingReturn.d.ts +31 -0
- package/dist/src/types/links/UseInAppLinkHandlingReturn.d.ts.map +1 -0
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -24,9 +24,11 @@ minimumReleaseAgeExclude:
|
|
|
24
24
|
| Subpath | Exports |
|
|
25
25
|
| --- | --- |
|
|
26
26
|
| `@favish/staffbase-utils/api` | `fetchJson`, `fetchAllPaginated`, `ApiError` |
|
|
27
|
+
| `@favish/staffbase-utils/content` | `resolveLocalizedContent`, `resolveActiveLanguage`, `detectEditorLanguage`, `detectPreviewLanguage` |
|
|
28
|
+
| `@favish/staffbase-utils/links/react` | `useInAppLinkHandling` (React peer) |
|
|
27
29
|
| `@favish/staffbase-utils/log` | `logError`, `logWarn`, `logDebug`, `setLoggingEnabled` |
|
|
28
30
|
| `@favish/staffbase-utils/dom` | `getDynamicClasses` |
|
|
29
|
-
| `@favish/staffbase-utils/types` | `Channel`, `ChannelLink`, `ChannelLinkParameter`, `DropdownOption` (type-only) |
|
|
31
|
+
| `@favish/staffbase-utils/types` | `Channel`, `ChannelLink`, `ChannelLinkParameter`, `DropdownOption`, `LocalizedContent`, `ArticleImage`, `ArticleImageVariant` (type-only) |
|
|
30
32
|
|
|
31
33
|
More modules (`/env`, `/device`, `/html`, `/links`, `/widgets`) are added per the
|
|
32
34
|
delivery roadmap; each is its own subpath so consumers only bundle what they import.
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});var e=()=>typeof document>`u`?null:(document.querySelector(`[aria-selected="true"]`)?.getAttribute(`data-testid`))?.split(`-`).pop()??null,t=()=>{if(typeof window>`u`)return null;let e=window.App?._urlParameters;return e?new URLSearchParams(e).get(`language`):null},n=n=>{let{contentLanguage:r,defaultLanguage:i,isEditor:a}=n,o=r??i;return a?e()??o:r??t()??o},r=(e,t)=>{let{defaultLanguage:r,onError:i}=t;if(!e)return i?.(`Invalid article data or missing contents`),null;let a=n(t),o=e[a]||e[r]||null;if(!o)return i?.(`Content unavailable for language: ${a} or fallback: ${r}`),null;let s=e[r];return{title:o.title||s?.title||``,teaser:o.teaser||s?.teaser||``,content:o.content||s?.content||``,image:o.image||s?.image||``,feedImage:o.feedImage||s?.feedImage||``}};exports.detectEditorLanguage=e,exports.detectPreviewLanguage=t,exports.resolveActiveLanguage=n,exports.resolveLocalizedContent=r;
|
|
2
|
+
//# sourceMappingURL=content.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.cjs.js","names":[],"sources":["../src/content/detectEditorLanguage.ts","../src/content/detectPreviewLanguage.ts","../src/content/resolveActiveLanguage.ts","../src/content/resolveLocalizedContent.ts"],"sourcesContent":["/**\n * Reads the active language from the Staffbase editor's selected language tab\n * (`[aria-selected=\"true\"]` with a `data-testid` suffixed by the language code).\n * Returns null when no tab is active or there is no DOM, so callers can fall back.\n * @returns {string | null} The detected editor language, or null.\n */\nexport const detectEditorLanguage = (): string | null => {\n if (typeof document === 'undefined') return null\n\n const activeTab = document.querySelector('[aria-selected=\"true\"]')\n const testId = activeTab?.getAttribute('data-testid')\n\n return testId?.split('-').pop() ?? null\n}\n","/**\n * Reads the `language` URL parameter the Staffbase preview shell exposes on\n * `window.App._urlParameters`. Returns null when the shell or parameter is\n * absent (or there is no DOM), so callers can fall back.\n * @returns {string | null} The preview language, or null.\n */\nexport const detectPreviewLanguage = (): string | null => {\n if (typeof window === 'undefined') return null\n\n const appConfig = (window as { App?: { _urlParameters?: string } }).App\n ?._urlParameters\n if (!appConfig) return null\n\n return new URLSearchParams(appConfig).get('language')\n}\n","import type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions'\nimport { detectEditorLanguage } from './detectEditorLanguage'\nimport { detectPreviewLanguage } from './detectPreviewLanguage'\n\n/**\n * Resolves the language to render content in.\n *\n * Editor path: the selected language tab wins. Runtime path: the explicit\n * `contentLanguage` is trusted first so the article language matches the rest of\n * the UI, with preview-URL sniffing only as a fallback. Both paths fall back to\n * `contentLanguage ?? defaultLanguage`.\n * @param {ResolveLocalizedContentOptions} options - The resolution options.\n * @returns {string} The resolved active language code.\n */\nexport const resolveActiveLanguage = (\n options: ResolveLocalizedContentOptions,\n): string => {\n const { contentLanguage, defaultLanguage, isEditor } = options\n const fallback = contentLanguage ?? defaultLanguage\n\n if (isEditor) return detectEditorLanguage() ?? fallback\n\n return contentLanguage ?? detectPreviewLanguage() ?? fallback\n}\n","import type { LocalizedContent } from '../types/content/LocalizedContent'\nimport type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions'\nimport { resolveActiveLanguage } from './resolveActiveLanguage'\n\n/**\n * Selects and field-merges the localized article content for the active language.\n *\n * Resolves the active language (see resolveActiveLanguage), picks that language's\n * content (falling back to the default language), then fills each field from the\n * default-language entry when the active one is empty. Returns null and reports\n * via `onError` when contents are missing or unavailable in both languages.\n * @param {Record<string, LocalizedContent> | undefined} contents - The article's per-language contents.\n * @param {ResolveLocalizedContentOptions} options - Language and diagnostics options.\n * @returns {LocalizedContent | null} The merged localized content, or null.\n */\nexport const resolveLocalizedContent = (\n contents: Record<string, LocalizedContent> | undefined,\n options: ResolveLocalizedContentOptions,\n): LocalizedContent | null => {\n const { defaultLanguage, onError } = options\n\n if (!contents) {\n onError?.('Invalid article data or missing contents')\n return null\n }\n\n const language = resolveActiveLanguage(options)\n const localized = contents[language] || contents[defaultLanguage] || null\n\n if (!localized) {\n onError?.(\n `Content unavailable for language: ${language} or fallback: ${defaultLanguage}`,\n )\n return null\n }\n\n const fallback = contents[defaultLanguage]\n\n return {\n title: localized.title || fallback?.title || '',\n teaser: localized.teaser || fallback?.teaser || '',\n content: localized.content || fallback?.content || '',\n image: localized.image || fallback?.image || '',\n feedImage: localized.feedImage || fallback?.feedImage || '',\n }\n}\n"],"mappings":"mEAMA,IAAa,MACP,OAAO,SAAa,IAAoB,MAE1B,SAAS,cAAc,wBAC1B,GAAW,aAAa,aAAa,IAErC,MAAM,GAAG,EAAE,IAAI,GAAK,KCNxB,MAA6C,CACxD,GAAI,OAAO,OAAW,IAAa,OAAO,KAE1C,IAAM,EAAa,OAAiD,KAChE,eAGJ,OAFK,EAEE,IAAI,gBAAgB,CAAS,EAAE,IAAI,UAAU,EAF7B,IAGzB,ECAa,EACX,GACW,CACX,GAAM,CAAE,kBAAiB,kBAAiB,YAAa,EACjD,EAAW,GAAmB,EAIpC,OAFI,EAAiB,EAAqB,GAAK,EAExC,GAAmB,EAAsB,GAAK,CACvD,ECRa,GACX,EACA,IAC4B,CAC5B,GAAM,CAAE,kBAAiB,WAAY,EAErC,GAAI,CAAC,EAEH,OADA,IAAU,0CAA0C,EAC7C,KAGT,IAAM,EAAW,EAAsB,CAAO,EACxC,EAAY,EAAS,IAAa,EAAS,IAAoB,KAErE,GAAI,CAAC,EAIH,OAHA,IACE,qCAAqC,EAAS,gBAAgB,GAChE,EACO,KAGT,IAAM,EAAW,EAAS,GAE1B,MAAO,CACL,MAAO,EAAU,OAAS,GAAU,OAAS,GAC7C,OAAQ,EAAU,QAAU,GAAU,QAAU,GAChD,QAAS,EAAU,SAAW,GAAU,SAAW,GACnD,MAAO,EAAU,OAAS,GAAU,OAAS,GAC7C,UAAW,EAAU,WAAa,GAAU,WAAa,EAC3D,CACF"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/content/detectEditorLanguage.ts
|
|
2
|
+
var e = () => typeof document > "u" ? null : (document.querySelector("[aria-selected=\"true\"]")?.getAttribute("data-testid"))?.split("-").pop() ?? null, t = () => {
|
|
3
|
+
if (typeof window > "u") return null;
|
|
4
|
+
let e = window.App?._urlParameters;
|
|
5
|
+
return e ? new URLSearchParams(e).get("language") : null;
|
|
6
|
+
}, n = (n) => {
|
|
7
|
+
let { contentLanguage: r, defaultLanguage: i, isEditor: a } = n, o = r ?? i;
|
|
8
|
+
return a ? e() ?? o : r ?? t() ?? o;
|
|
9
|
+
}, r = (e, t) => {
|
|
10
|
+
let { defaultLanguage: r, onError: i } = t;
|
|
11
|
+
if (!e) return i?.("Invalid article data or missing contents"), null;
|
|
12
|
+
let a = n(t), o = e[a] || e[r] || null;
|
|
13
|
+
if (!o) return i?.(`Content unavailable for language: ${a} or fallback: ${r}`), null;
|
|
14
|
+
let s = e[r];
|
|
15
|
+
return {
|
|
16
|
+
title: o.title || s?.title || "",
|
|
17
|
+
teaser: o.teaser || s?.teaser || "",
|
|
18
|
+
content: o.content || s?.content || "",
|
|
19
|
+
image: o.image || s?.image || "",
|
|
20
|
+
feedImage: o.feedImage || s?.feedImage || ""
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { e as detectEditorLanguage, t as detectPreviewLanguage, n as resolveActiveLanguage, r as resolveLocalizedContent };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=content.es.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.es.mjs","names":[],"sources":["../src/content/detectEditorLanguage.ts","../src/content/detectPreviewLanguage.ts","../src/content/resolveActiveLanguage.ts","../src/content/resolveLocalizedContent.ts"],"sourcesContent":["/**\n * Reads the active language from the Staffbase editor's selected language tab\n * (`[aria-selected=\"true\"]` with a `data-testid` suffixed by the language code).\n * Returns null when no tab is active or there is no DOM, so callers can fall back.\n * @returns {string | null} The detected editor language, or null.\n */\nexport const detectEditorLanguage = (): string | null => {\n if (typeof document === 'undefined') return null\n\n const activeTab = document.querySelector('[aria-selected=\"true\"]')\n const testId = activeTab?.getAttribute('data-testid')\n\n return testId?.split('-').pop() ?? null\n}\n","/**\n * Reads the `language` URL parameter the Staffbase preview shell exposes on\n * `window.App._urlParameters`. Returns null when the shell or parameter is\n * absent (or there is no DOM), so callers can fall back.\n * @returns {string | null} The preview language, or null.\n */\nexport const detectPreviewLanguage = (): string | null => {\n if (typeof window === 'undefined') return null\n\n const appConfig = (window as { App?: { _urlParameters?: string } }).App\n ?._urlParameters\n if (!appConfig) return null\n\n return new URLSearchParams(appConfig).get('language')\n}\n","import type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions'\nimport { detectEditorLanguage } from './detectEditorLanguage'\nimport { detectPreviewLanguage } from './detectPreviewLanguage'\n\n/**\n * Resolves the language to render content in.\n *\n * Editor path: the selected language tab wins. Runtime path: the explicit\n * `contentLanguage` is trusted first so the article language matches the rest of\n * the UI, with preview-URL sniffing only as a fallback. Both paths fall back to\n * `contentLanguage ?? defaultLanguage`.\n * @param {ResolveLocalizedContentOptions} options - The resolution options.\n * @returns {string} The resolved active language code.\n */\nexport const resolveActiveLanguage = (\n options: ResolveLocalizedContentOptions,\n): string => {\n const { contentLanguage, defaultLanguage, isEditor } = options\n const fallback = contentLanguage ?? defaultLanguage\n\n if (isEditor) return detectEditorLanguage() ?? fallback\n\n return contentLanguage ?? detectPreviewLanguage() ?? fallback\n}\n","import type { LocalizedContent } from '../types/content/LocalizedContent'\nimport type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions'\nimport { resolveActiveLanguage } from './resolveActiveLanguage'\n\n/**\n * Selects and field-merges the localized article content for the active language.\n *\n * Resolves the active language (see resolveActiveLanguage), picks that language's\n * content (falling back to the default language), then fills each field from the\n * default-language entry when the active one is empty. Returns null and reports\n * via `onError` when contents are missing or unavailable in both languages.\n * @param {Record<string, LocalizedContent> | undefined} contents - The article's per-language contents.\n * @param {ResolveLocalizedContentOptions} options - Language and diagnostics options.\n * @returns {LocalizedContent | null} The merged localized content, or null.\n */\nexport const resolveLocalizedContent = (\n contents: Record<string, LocalizedContent> | undefined,\n options: ResolveLocalizedContentOptions,\n): LocalizedContent | null => {\n const { defaultLanguage, onError } = options\n\n if (!contents) {\n onError?.('Invalid article data or missing contents')\n return null\n }\n\n const language = resolveActiveLanguage(options)\n const localized = contents[language] || contents[defaultLanguage] || null\n\n if (!localized) {\n onError?.(\n `Content unavailable for language: ${language} or fallback: ${defaultLanguage}`,\n )\n return null\n }\n\n const fallback = contents[defaultLanguage]\n\n return {\n title: localized.title || fallback?.title || '',\n teaser: localized.teaser || fallback?.teaser || '',\n content: localized.content || fallback?.content || '',\n image: localized.image || fallback?.image || '',\n feedImage: localized.feedImage || fallback?.feedImage || '',\n }\n}\n"],"mappings":";AAMA,IAAa,UACP,OAAO,WAAa,MAAoB,QAE1B,SAAS,cAAc,0BAC1B,GAAW,aAAa,aAAa,IAErC,MAAM,GAAG,EAAE,IAAI,KAAK,MCNxB,UAA6C;CACxD,IAAI,OAAO,SAAW,KAAa,OAAO;CAE1C,IAAM,IAAa,OAAiD,KAChE;CAGJ,OAFK,IAEE,IAAI,gBAAgB,CAAS,EAAE,IAAI,UAAU,IAF7B;AAGzB,GCAa,KACX,MACW;CACX,IAAM,EAAE,oBAAiB,oBAAiB,gBAAa,GACjD,IAAW,KAAmB;CAIpC,OAFI,IAAiB,EAAqB,KAAK,IAExC,KAAmB,EAAsB,KAAK;AACvD,GCRa,KACX,GACA,MAC4B;CAC5B,IAAM,EAAE,oBAAiB,eAAY;CAErC,IAAI,CAAC,GAEH,OADA,IAAU,0CAA0C,GAC7C;CAGT,IAAM,IAAW,EAAsB,CAAO,GACxC,IAAY,EAAS,MAAa,EAAS,MAAoB;CAErE,IAAI,CAAC,GAIH,OAHA,IACE,qCAAqC,EAAS,gBAAgB,GAChE,GACO;CAGT,IAAM,IAAW,EAAS;CAE1B,OAAO;EACL,OAAO,EAAU,SAAS,GAAU,SAAS;EAC7C,QAAQ,EAAU,UAAU,GAAU,UAAU;EAChD,SAAS,EAAU,WAAW,GAAU,WAAW;EACnD,OAAO,EAAU,SAAS,GAAU,SAAS;EAC7C,WAAW,EAAU,aAAa,GAAU,aAAa;CAC3D;AACF"}
|
|
@@ -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`});
|
|
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
|
package/dist/links.cjs.js.map
CHANGED
|
@@ -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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
],
|
|
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",
|
|
30
|
-
if (
|
|
31
|
-
if (
|
|
32
|
-
let
|
|
33
|
-
return
|
|
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 {
|
|
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
|
package/dist/links.es.mjs.map
CHANGED
|
@@ -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
|
+
/**
|
|
2
|
+
* Reads the active language from the Staffbase editor's selected language tab
|
|
3
|
+
* (`[aria-selected="true"]` with a `data-testid` suffixed by the language code).
|
|
4
|
+
* Returns null when no tab is active or there is no DOM, so callers can fall back.
|
|
5
|
+
* @returns {string | null} The detected editor language, or null.
|
|
6
|
+
*/
|
|
7
|
+
export declare const detectEditorLanguage: () => string | null;
|
|
8
|
+
//# sourceMappingURL=detectEditorLanguage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectEditorLanguage.d.ts","sourceRoot":"","sources":["../../../src/content/detectEditorLanguage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAO,MAAM,GAAG,IAOhD,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the `language` URL parameter the Staffbase preview shell exposes on
|
|
3
|
+
* `window.App._urlParameters`. Returns null when the shell or parameter is
|
|
4
|
+
* absent (or there is no DOM), so callers can fall back.
|
|
5
|
+
* @returns {string | null} The preview language, or null.
|
|
6
|
+
*/
|
|
7
|
+
export declare const detectPreviewLanguage: () => string | null;
|
|
8
|
+
//# sourceMappingURL=detectPreviewLanguage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"detectPreviewLanguage.d.ts","sourceRoot":"","sources":["../../../src/content/detectPreviewLanguage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,QAAO,MAAM,GAAG,IAQjD,CAAA"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { detectEditorLanguage } from './detectEditorLanguage';
|
|
2
|
+
export { detectPreviewLanguage } from './detectPreviewLanguage';
|
|
3
|
+
export { resolveActiveLanguage } from './resolveActiveLanguage';
|
|
4
|
+
export { resolveLocalizedContent } from './resolveLocalizedContent';
|
|
5
|
+
export type { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions';
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/content/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAA;AACnE,YAAY,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions';
|
|
2
|
+
/**
|
|
3
|
+
* Resolves the language to render content in.
|
|
4
|
+
*
|
|
5
|
+
* Editor path: the selected language tab wins. Runtime path: the explicit
|
|
6
|
+
* `contentLanguage` is trusted first so the article language matches the rest of
|
|
7
|
+
* the UI, with preview-URL sniffing only as a fallback. Both paths fall back to
|
|
8
|
+
* `contentLanguage ?? defaultLanguage`.
|
|
9
|
+
* @param {ResolveLocalizedContentOptions} options - The resolution options.
|
|
10
|
+
* @returns {string} The resolved active language code.
|
|
11
|
+
*/
|
|
12
|
+
export declare const resolveActiveLanguage: (options: ResolveLocalizedContentOptions) => string;
|
|
13
|
+
//# sourceMappingURL=resolveActiveLanguage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveActiveLanguage.d.ts","sourceRoot":"","sources":["../../../src/content/resolveActiveLanguage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA;AAIrG;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GAChC,SAAS,8BAA8B,KACtC,MAOF,CAAA"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LocalizedContent } from '../types/content/LocalizedContent';
|
|
2
|
+
import { ResolveLocalizedContentOptions } from '../types/content/ResolveLocalizedContentOptions';
|
|
3
|
+
/**
|
|
4
|
+
* Selects and field-merges the localized article content for the active language.
|
|
5
|
+
*
|
|
6
|
+
* Resolves the active language (see resolveActiveLanguage), picks that language's
|
|
7
|
+
* content (falling back to the default language), then fills each field from the
|
|
8
|
+
* default-language entry when the active one is empty. Returns null and reports
|
|
9
|
+
* via `onError` when contents are missing or unavailable in both languages.
|
|
10
|
+
* @param {Record<string, LocalizedContent> | undefined} contents - The article's per-language contents.
|
|
11
|
+
* @param {ResolveLocalizedContentOptions} options - Language and diagnostics options.
|
|
12
|
+
* @returns {LocalizedContent | null} The merged localized content, or null.
|
|
13
|
+
*/
|
|
14
|
+
export declare const resolveLocalizedContent: (contents: Record<string, LocalizedContent> | undefined, options: ResolveLocalizedContentOptions) => LocalizedContent | null;
|
|
15
|
+
//# sourceMappingURL=resolveLocalizedContent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveLocalizedContent.d.ts","sourceRoot":"","sources":["../../../src/content/resolveLocalizedContent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACzE,OAAO,KAAK,EAAE,8BAA8B,EAAE,MAAM,iDAAiD,CAAA;AAGrG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,GAAG,SAAS,EACtD,SAAS,8BAA8B,KACtC,gBAAgB,GAAG,IA2BrB,CAAA"}
|
|
@@ -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,17 @@
|
|
|
1
|
+
import { ArticleImageVariant } from './ArticleImageVariant';
|
|
2
|
+
/**
|
|
3
|
+
* The set of rendition variants the Staffbase media API returns for an article
|
|
4
|
+
* image. Named `ArticleImage` (not `Image`) to avoid shadowing the DOM global.
|
|
5
|
+
*/
|
|
6
|
+
export interface ArticleImage {
|
|
7
|
+
original: ArticleImageVariant & {
|
|
8
|
+
size: number;
|
|
9
|
+
};
|
|
10
|
+
original_scaled: ArticleImageVariant;
|
|
11
|
+
thumb: ArticleImageVariant;
|
|
12
|
+
wide: ArticleImageVariant;
|
|
13
|
+
compact: ArticleImageVariant;
|
|
14
|
+
wide_first: ArticleImageVariant;
|
|
15
|
+
compact_first: ArticleImageVariant;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=ArticleImage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ArticleImage.d.ts","sourceRoot":"","sources":["../../../../src/types/content/ArticleImage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAEhE;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,mBAAmB,GAAG;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IAChD,eAAe,EAAE,mBAAmB,CAAA;IACpC,KAAK,EAAE,mBAAmB,CAAA;IAC1B,IAAI,EAAE,mBAAmB,CAAA;IACzB,OAAO,EAAE,mBAAmB,CAAA;IAC5B,UAAU,EAAE,mBAAmB,CAAA;IAC/B,aAAa,EAAE,mBAAmB,CAAA;CACnC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A single rendition of an article image (a size/format variant returned by the
|
|
3
|
+
* Staffbase media API). `size` is only present on the original variant.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArticleImageVariant {
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
size?: number;
|
|
9
|
+
format: string | null;
|
|
10
|
+
mimeType: string | null;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=ArticleImageVariant.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ArticleImageVariant.d.ts","sourceRoot":"","sources":["../../../../src/types/content/ArticleImageVariant.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,GAAG,EAAE,MAAM,CAAA;CACZ"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ArticleImage } from './ArticleImage';
|
|
2
|
+
/**
|
|
3
|
+
* The per-language content of an article (one entry of `ArticleData.contents`).
|
|
4
|
+
*
|
|
5
|
+
* `image` is `ArticleImage | string | null` because the API returns either the
|
|
6
|
+
* structured rendition set, a bare URL string, or nothing. Shared verbatim by
|
|
7
|
+
* the alerts and unacknowledged-bulletins widgets.
|
|
8
|
+
*/
|
|
9
|
+
export interface LocalizedContent {
|
|
10
|
+
title: string;
|
|
11
|
+
teaser: string;
|
|
12
|
+
content: string;
|
|
13
|
+
image: ArticleImage | string | null;
|
|
14
|
+
feedImage: string | null;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=LocalizedContent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalizedContent.d.ts","sourceRoot":"","sources":["../../../../src/types/content/LocalizedContent.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAElD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,IAAI,CAAA;IACnC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for resolveLocalizedContent.
|
|
3
|
+
*/
|
|
4
|
+
export interface ResolveLocalizedContentOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Language explicitly requested by the host/widget config. On the runtime path
|
|
7
|
+
* this is trusted first so the article language matches the rest of the UI;
|
|
8
|
+
* DOM/URL sniffing is only a fallback.
|
|
9
|
+
*/
|
|
10
|
+
contentLanguage?: string;
|
|
11
|
+
/** Fallback language. Injected because the library never reads env. */
|
|
12
|
+
defaultLanguage: string;
|
|
13
|
+
/** When true, resolves the active language from the editor's selected tab. */
|
|
14
|
+
isEditor?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Called with a diagnostic message when content cannot be resolved (the
|
|
17
|
+
* library does not log directly).
|
|
18
|
+
* @param {string} message - The diagnostic message.
|
|
19
|
+
* @param {...unknown} args - Optional extra context.
|
|
20
|
+
* @returns {void} Nothing.
|
|
21
|
+
*/
|
|
22
|
+
onError?: (message: string, ...args: unknown[]) => void;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=ResolveLocalizedContentOptions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ResolveLocalizedContentOptions.d.ts","sourceRoot":"","sources":["../../../../src/types/content/ResolveLocalizedContentOptions.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,8BAA8B;IAC7C;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,uEAAuE;IACvE,eAAe,EAAE,MAAM,CAAA;IACvB,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;CACxD"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
export type { ArticleImage } from './ArticleImage';
|
|
2
|
+
export type { ArticleImageVariant } from './ArticleImageVariant';
|
|
1
3
|
export type { Channel } from './Channel';
|
|
2
4
|
export type { ChannelLink } from './ChannelLink';
|
|
3
5
|
export type { ChannelLinkParameter } from './ChannelLinkParameter';
|
|
4
6
|
export type { DropdownOption } from './DropdownOption';
|
|
7
|
+
export type { LocalizedContent } from './LocalizedContent';
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/types/content/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAClE,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/types/content/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAClD,YAAY,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAA;AAChE,YAAY,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACxC,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAChD,YAAY,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAClE,YAAY,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AACtD,YAAY,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAA"}
|
|
@@ -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.
|
|
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",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"import": "./dist/api.es.mjs",
|
|
14
14
|
"require": "./dist/api.cjs.js"
|
|
15
15
|
},
|
|
16
|
+
"./content": {
|
|
17
|
+
"types": "./dist/src/content/index.d.ts",
|
|
18
|
+
"import": "./dist/content.es.mjs",
|
|
19
|
+
"require": "./dist/content.cjs.js"
|
|
20
|
+
},
|
|
16
21
|
"./log": {
|
|
17
22
|
"types": "./dist/src/log/index.d.ts",
|
|
18
23
|
"import": "./dist/log.es.mjs",
|
|
@@ -43,6 +48,11 @@
|
|
|
43
48
|
"import": "./dist/widgets.es.mjs",
|
|
44
49
|
"require": "./dist/widgets.cjs.js"
|
|
45
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
|
+
},
|
|
46
56
|
"./widgets/react": {
|
|
47
57
|
"types": "./dist/src/widgets/react/index.d.ts",
|
|
48
58
|
"import": "./dist/widgets/react.es.mjs",
|