@clearly-hub/browser 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -9
- package/dist/clearly-hub.js +2 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +46 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,14 +59,15 @@ Or `npm install @clearly-hub/browser` and `import { clearlyHub } from '@clearly-
|
|
|
59
59
|
|
|
60
60
|
## The browser package
|
|
61
61
|
|
|
62
|
-
| |
|
|
63
|
-
| ------------------------------------------------ |
|
|
64
|
-
| `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`)
|
|
65
|
-
| `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page
|
|
66
|
-
| `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there
|
|
67
|
-
| `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to
|
|
68
|
-
|
|
|
69
|
-
|
|
|
62
|
+
| | |
|
|
63
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
64
|
+
| `clearlyHub.session()` | `{ signedIn: false }`, or the signed-in user (`sub`, `email`, `name`, `appUserId`) |
|
|
65
|
+
| `clearlyHub.signIn()`, `signOut()` | Go through Clearly.HUB and come back to this page |
|
|
66
|
+
| `clearlyHub.open(action, { data, display })` | Opens a hosted screen; resolves with its checked answer. Call it from a click: browsers only allow popups there |
|
|
67
|
+
| `clearlyHub.result()` | After a redirect screen: its answer, on the page it came back to |
|
|
68
|
+
| `clearlyHub.onAnswer(callback)` | Every answer, once each, from a popup or after a redirect; also when you start listening after it arrived (React). Returns a function that stops listening |
|
|
69
|
+
| `<clearly-hub-button action display data label>` | Branded button for `sign-in`, `sign-out` and each screen |
|
|
70
|
+
| `clearly-hub-answer` event | Every answer, from a popup or after a redirect |
|
|
70
71
|
|
|
71
72
|
**Styling the button.** It uses your page's font. Attributes for the common cases:
|
|
72
73
|
`variant="outline"`, `size="small"` or `size="large"`. CSS custom properties for your look:
|
|
@@ -131,7 +132,6 @@ store; it's split over several cookies because tokens are large.
|
|
|
131
132
|
|
|
132
133
|
- Setting up your own workspaces or tenants: do it by hand, with plans that need approval, or in
|
|
133
134
|
`onAnswer` for `SELECT_SUBSCRIPTION`.
|
|
134
|
-
- Child hubs in `session().hubs`: Clearly.HUB only returns root hubs today.
|
|
135
135
|
- Other server languages: follow the Build checklist.
|
|
136
136
|
|
|
137
137
|
## Working on this repo
|
package/dist/clearly-hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"use strict";(()=>{var C=Object.defineProperty;var
|
|
1
|
+
"use strict";(()=>{var C=Object.defineProperty;var T=(r,t,n)=>t in r?C(r,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):r[t]=n;var a=(r,t,n)=>T(r,typeof t!="symbol"?t+"":t,n);var x=["SELECT_SUBSCRIPTION","DATA_PICKER","REGISTER_RESOURCE","SELECT_HUB"];function E(r){return x.includes(r)}function S(r,t){if(r!=="REGISTER_RESOURCE")return;let{url:n,format:i}=t??{};if(typeof n!="string"||!n||typeof i!="string"||!i)return'REGISTER_RESOURCE needs data with a url and a format, for example { url, format: "geojson" }';let o;try{o=new URL(n).hostname}catch{return`REGISTER_RESOURCE: "${n}" is not a URL`}if(!o.includes("."))return"REGISTER_RESOURCE needs a url with a domain Clearly.HUB accepts, not localhost"}var R="/clearly-hub";var c={signIn:"/sign-in",callback:"/callback",signOut:"/sign-out",session:"/session",open:"/open",answer:"/answer",result:"/result"},b="clearly-hub-answers";var u="clearly-hub-answer",y=480,h=class{constructor(t={}){a(this,"basePath",R);a(this,"display","popup");a(this,"redirectResult");a(this,"unclaimedRedirectResult");this.configure(t)}configure(t){return t.basePath&&(this.basePath=t.basePath.replace(/\/+$/,"")),t.display&&(this.display=t.display),this}async session(){let t=await fetch(this.url(c.session),{credentials:"same-origin"});if(!t.ok)throw new Error(`Clearly.HUB session failed (${t.status})`);return t.json()}signIn({returnTo:t=m()}={}){window.location.assign(this.url(c.signIn,{returnTo:t}))}signOut(){window.location.assign(this.url(c.signOut))}open(t,n={}){let i=S(t,n.data);if(i)return Promise.reject(new Error(`Clearly.HUB: ${i}`));let o=crypto.randomUUID(),e=n.display??this.display,s={action:t,ref:o,returnTo:n.returnTo??m(),data:n.data?JSON.stringify(n.data):void 0};if(e==="popup"){let l=window.open(this.url(c.open,{...s,display:"popup"}),"clearly-hub-screen",`width=${y},height=${screen.availHeight},left=${screen.availWidth-y}`);if(l)return this.waitForPopup(l,o,t,n.eventTarget??window)}return window.location.assign(this.url(c.open,{...s,display:"redirect"})),new Promise(()=>{})}onAnswer(t){let n=o=>{let e=o.detail;e===this.unclaimedRedirectResult&&(this.unclaimedRedirectResult=void 0),t(e)};window.addEventListener(u,n);let i=this.unclaimedRedirectResult;return i&&(this.unclaimedRedirectResult=void 0,t(i)),()=>window.removeEventListener(u,n)}deliverRedirectResult(t){this.unclaimedRedirectResult=t,window.dispatchEvent(new CustomEvent(u,{detail:t}))}result(){return this.redirectResult??(this.redirectResult=fetch(this.url(c.result),{credentials:"same-origin"}).then(t=>t.ok?t.json():null).then(t=>t?.result??null).catch(()=>null)),this.redirectResult}waitForPopup(t,n,i,o){return new Promise(e=>{let s=new BroadcastChannel(b),l=!1,f=p=>{l||(l=!0,s.close(),window.clearInterval(g),o.dispatchEvent(new CustomEvent(u,{detail:p,bubbles:!0,composed:!0})),e(p))};s.onmessage=p=>{p.data?.ref===n&&f(p.data.result)};let g=window.setInterval(()=>{t.closed&&(window.clearInterval(g),window.setTimeout(()=>f({action:i,result:"CANCELLED"}),500))},400)})}url(t,n={}){let i=new URLSearchParams;for(let[e,s]of Object.entries(n))s!==void 0&&i.set(e,s);let o=i.toString();return`${this.basePath}${t}${o?`?${o}`:""}`}};function m(){return`${window.location.pathname}${window.location.search}${window.location.hash}`}var w={"sign-in":"Sign in with Clearly.HUB","sign-out":"Sign out",SELECT_SUBSCRIPTION:"Choose a plan in Clearly.HUB",DATA_PICKER:"Pick data from Clearly.HUB",REGISTER_RESOURCE:"Save to Clearly.HUB",SELECT_HUB:"Choose a hub"},O=`
|
|
2
2
|
:host { display: inline-block; }
|
|
3
3
|
button {
|
|
4
4
|
--color: var(--clearly-hub-color, #1fa69d);
|
|
@@ -20,4 +20,4 @@
|
|
|
20
20
|
button:focus-visible { outline: 2px solid var(--color); outline-offset: 2px; }
|
|
21
21
|
button:disabled { opacity: 0.6; cursor: default; }
|
|
22
22
|
svg { width: 1.25em; height: 1.25em; flex: none; fill: currentColor; }
|
|
23
|
-
`,
|
|
23
|
+
`,U='<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M21 6.5c-1.66 0-3 1.34-3 3 0 .07 0 .14.01.21l-2.03.68c-.64-1.21-1.82-2.09-3.22-2.32V5.91C14.04 5.57 15 4.4 15 3c0-1.66-1.34-3-3-3S9 1.34 9 3c0 1.4.96 2.57 2.25 2.91v2.16c-1.4.23-2.58 1.11-3.22 2.32l-2.04-.68C6 9.64 6 9.57 6 9.5c0-1.66-1.34-3-3-3s-3 1.34-3 3 1.34 3 3 3c1.06 0 1.98-.55 2.52-1.37l2.03.68c-.2 1.29.17 2.66 1.09 3.69l-1.41 1.77Q6.66 17 6 17c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3c0-.68-.22-1.3-.6-1.8l1.41-1.77c1.36.76 3.02.75 4.37 0l1.41 1.77c-.37.5-.59 1.12-.59 1.8 0 1.66 1.34 3 3 3s3-1.34 3-3-1.34-3-3-3c-.44 0-.85.09-1.23.26l-1.41-1.77c.93-1.04 1.29-2.4 1.09-3.69l2.03-.68c.53.82 1.46 1.37 2.52 1.37 1.66 0 3-1.34 3-3S22.66 6.5 21 6.5m-18 4c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1M6 21c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1m5-18c0-.55.45-1 1-1s1 .45 1 1-.45 1-1 1-1-.45-1-1m1 12c-1.38 0-2.5-1.12-2.5-2.5S10.62 10 12 10s2.5 1.12 2.5 2.5S13.38 15 12 15m6 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1m3-8.5c-.55 0-1-.45-1-1s.45-1 1-1 1 .45 1 1-.45 1-1 1"/></svg>';function A(r,t="clearly-hub-button"){if(typeof customElements>"u"||customElements.get(t))return;class n extends HTMLElement{constructor(){super();a(this,"button");let e=this.attachShadow({mode:"open"});e.innerHTML=`<style>${O}</style><button part="button" type="button"></button>`,this.button=e.querySelector("button"),this.button.addEventListener("click",()=>this.run())}connectedCallback(){for(let e of["action","display","label","variant","size","data"])if(Object.prototype.hasOwnProperty.call(this,e)){let s=this[e];delete this[e],this[e]=s}this.render()}attributeChangedCallback(){this.render()}get action(){let e=this.getAttribute("action")??"sign-in";return e in w?e:"sign-in"}set action(e){this.setOrRemove("action",e)}get display(){return this.getAttribute("display")==="redirect"?"redirect":void 0}set display(e){this.setOrRemove("display",e)}get label(){return this.getAttribute("label")}set label(e){this.setOrRemove("label",e)}get variant(){return this.getAttribute("variant")}set variant(e){this.setOrRemove("variant",e)}get size(){return this.getAttribute("size")}set size(e){this.setOrRemove("size",e)}get data(){let e=this.getAttribute("data");return e?JSON.parse(e):void 0}set data(e){this.setOrRemove("data",typeof e=="string"||e==null?e:JSON.stringify(e))}setOrRemove(e,s){s==null?this.removeAttribute(e):this.setAttribute(e,s)}render(){let e=this.label??w[this.action];this.button.innerHTML=`${U}<span></span>`,this.button.querySelector("span").textContent=e}async run(){let e=this.action;if(e==="sign-in")return r.signIn();if(e==="sign-out")return r.signOut();if(E(e)){this.button.disabled=!0;try{await r.open(e,{display:this.display,data:this.data,eventTarget:this})}catch(s){console.error(s);let l={action:e,result:"FAILURE",error:s.message};this.dispatchEvent(new CustomEvent(u,{detail:l,bubbles:!0,composed:!0}))}finally{this.button.disabled=!1}}}}a(n,"observedAttributes",["action","label"]),customElements.define(t,n)}var d=new h;A(d);if(typeof window<"u"){let r=()=>{d.result().then(t=>{t&&d.deliverRedirectResult(t)})};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",r,{once:!0}):queueMicrotask(r)}var v=document.currentScript;d.configure({basePath:v?.dataset.basePath,display:v?.dataset.display==="redirect"?"redirect":void 0});window.clearlyHub=d;})();
|
package/dist/index.d.ts
CHANGED
|
@@ -47,12 +47,14 @@ interface OpenOptions<A extends ScreenAction> {
|
|
|
47
47
|
display?: ScreenDisplay;
|
|
48
48
|
data?: A extends keyof ScreenData ? ScreenData[A] : never;
|
|
49
49
|
returnTo?: string;
|
|
50
|
+
eventTarget?: EventTarget;
|
|
50
51
|
}
|
|
51
52
|
declare const ANSWER_EVENT = "clearly-hub-answer";
|
|
52
53
|
declare class ClearlyHubClient {
|
|
53
54
|
private basePath;
|
|
54
55
|
private display;
|
|
55
56
|
private redirectResult?;
|
|
57
|
+
private unclaimedRedirectResult?;
|
|
56
58
|
constructor(options?: ClientOptions);
|
|
57
59
|
configure(options: ClientOptions): this;
|
|
58
60
|
session(): Promise<Session>;
|
|
@@ -61,6 +63,8 @@ declare class ClearlyHubClient {
|
|
|
61
63
|
}): void;
|
|
62
64
|
signOut(): void;
|
|
63
65
|
open<A extends ScreenAction>(action: A, options?: OpenOptions<A>): Promise<ScreenResult<A>>;
|
|
66
|
+
onAnswer(callback: (result: ScreenResult) => void): () => void;
|
|
67
|
+
deliverRedirectResult(result: ScreenResult): void;
|
|
64
68
|
result(): Promise<ScreenResult | null>;
|
|
65
69
|
private waitForPopup;
|
|
66
70
|
private url;
|
package/dist/index.js
CHANGED
|
@@ -51,6 +51,9 @@ var ClearlyHubClient = class {
|
|
|
51
51
|
__publicField(this, "basePath", DEFAULT_BASE_PATH);
|
|
52
52
|
__publicField(this, "display", "popup");
|
|
53
53
|
__publicField(this, "redirectResult");
|
|
54
|
+
// An answer after a redirect that no onAnswer() callback has had yet: the
|
|
55
|
+
// first callback to come gets it
|
|
56
|
+
__publicField(this, "unclaimedRedirectResult");
|
|
54
57
|
this.configure(options);
|
|
55
58
|
}
|
|
56
59
|
configure(options) {
|
|
@@ -90,19 +93,42 @@ var ClearlyHubClient = class {
|
|
|
90
93
|
"clearly-hub-screen",
|
|
91
94
|
`width=${POPUP_WIDTH},height=${screen.availHeight},left=${screen.availWidth - POPUP_WIDTH}`
|
|
92
95
|
);
|
|
93
|
-
if (popup) return this.waitForPopup(popup, ref, action);
|
|
96
|
+
if (popup) return this.waitForPopup(popup, ref, action, options.eventTarget ?? window);
|
|
94
97
|
}
|
|
95
98
|
window.location.assign(this.url(ROUTES.open, { ...query, display: "redirect" }));
|
|
96
99
|
return new Promise(() => {
|
|
97
100
|
});
|
|
98
101
|
}
|
|
102
|
+
// Every answer, once each: from a popup, and after a redirect, also when
|
|
103
|
+
// that one arrived before you called this (as in React, where listeners
|
|
104
|
+
// come after the first render). Returns a function that stops listening.
|
|
105
|
+
onAnswer(callback) {
|
|
106
|
+
const listener = (event) => {
|
|
107
|
+
const result = event.detail;
|
|
108
|
+
if (result === this.unclaimedRedirectResult) this.unclaimedRedirectResult = void 0;
|
|
109
|
+
callback(result);
|
|
110
|
+
};
|
|
111
|
+
window.addEventListener(ANSWER_EVENT, listener);
|
|
112
|
+
const unclaimed = this.unclaimedRedirectResult;
|
|
113
|
+
if (unclaimed) {
|
|
114
|
+
this.unclaimedRedirectResult = void 0;
|
|
115
|
+
callback(unclaimed);
|
|
116
|
+
}
|
|
117
|
+
return () => window.removeEventListener(ANSWER_EVENT, listener);
|
|
118
|
+
}
|
|
119
|
+
// On page load: announces the answer after a redirect as the
|
|
120
|
+
// clearly-hub-answer event, and keeps it for an onAnswer() that comes later
|
|
121
|
+
deliverRedirectResult(result) {
|
|
122
|
+
this.unclaimedRedirectResult = result;
|
|
123
|
+
window.dispatchEvent(new CustomEvent(ANSWER_EVENT, { detail: result }));
|
|
124
|
+
}
|
|
99
125
|
// After a redirect screen: its answer, once, on the page it came back to.
|
|
100
126
|
// Null when there is none.
|
|
101
127
|
result() {
|
|
102
128
|
this.redirectResult ?? (this.redirectResult = fetch(this.url(ROUTES.result), { credentials: "same-origin" }).then((response) => response.ok ? response.json() : null).then((message) => message?.result ?? null).catch(() => null));
|
|
103
129
|
return this.redirectResult;
|
|
104
130
|
}
|
|
105
|
-
waitForPopup(popup, ref, action) {
|
|
131
|
+
waitForPopup(popup, ref, action, eventTarget) {
|
|
106
132
|
return new Promise((resolve) => {
|
|
107
133
|
const channel = new BroadcastChannel(ANSWER_CHANNEL);
|
|
108
134
|
let isDone = false;
|
|
@@ -111,7 +137,9 @@ var ClearlyHubClient = class {
|
|
|
111
137
|
isDone = true;
|
|
112
138
|
channel.close();
|
|
113
139
|
window.clearInterval(closedCheck);
|
|
114
|
-
|
|
140
|
+
eventTarget.dispatchEvent(
|
|
141
|
+
new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
|
|
142
|
+
);
|
|
115
143
|
resolve(result);
|
|
116
144
|
};
|
|
117
145
|
channel.onmessage = (event) => {
|
|
@@ -254,21 +282,21 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
254
282
|
if (action === "sign-out") return client.signOut();
|
|
255
283
|
if (!isScreenAction(action)) return;
|
|
256
284
|
this.button.disabled = true;
|
|
257
|
-
let result;
|
|
258
285
|
try {
|
|
259
|
-
|
|
286
|
+
await client.open(action, {
|
|
260
287
|
display: this.display,
|
|
261
|
-
data: this.data
|
|
288
|
+
data: this.data,
|
|
289
|
+
eventTarget: this
|
|
262
290
|
});
|
|
263
291
|
} catch (error) {
|
|
264
292
|
console.error(error);
|
|
265
|
-
result = { action, result: "FAILURE", error: error.message };
|
|
293
|
+
const result = { action, result: "FAILURE", error: error.message };
|
|
294
|
+
this.dispatchEvent(
|
|
295
|
+
new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
|
|
296
|
+
);
|
|
266
297
|
} finally {
|
|
267
298
|
this.button.disabled = false;
|
|
268
299
|
}
|
|
269
|
-
this.dispatchEvent(
|
|
270
|
-
new CustomEvent(ANSWER_EVENT, { detail: result, bubbles: true, composed: true })
|
|
271
|
-
);
|
|
272
300
|
}
|
|
273
301
|
}
|
|
274
302
|
__publicField(ClearlyHubButton, "observedAttributes", ["action", "label"]);
|
|
@@ -279,11 +307,16 @@ function defineButton(client, tagName = "clearly-hub-button") {
|
|
|
279
307
|
var clearlyHub = new ClearlyHubClient();
|
|
280
308
|
defineButton(clearlyHub);
|
|
281
309
|
if (typeof window !== "undefined") {
|
|
282
|
-
|
|
310
|
+
const deliver = () => {
|
|
283
311
|
void clearlyHub.result().then((result) => {
|
|
284
|
-
if (result)
|
|
312
|
+
if (result) clearlyHub.deliverRedirectResult(result);
|
|
285
313
|
});
|
|
286
|
-
}
|
|
314
|
+
};
|
|
315
|
+
if (document.readyState === "loading") {
|
|
316
|
+
document.addEventListener("DOMContentLoaded", deliver, { once: true });
|
|
317
|
+
} else {
|
|
318
|
+
queueMicrotask(deliver);
|
|
319
|
+
}
|
|
287
320
|
}
|
|
288
321
|
export {
|
|
289
322
|
ANSWER_EVENT,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clearly-hub/browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Clearly.HUB in the browser: sign-in, hosted screens and the session, talking to @clearly-hub/node",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"author": "Future Insight Group BV",
|