@morgan-and-morgan/intake-agent 0.1.0-alpha.1 → 1.0.0-alpha.10
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 +26 -1
- package/dist/button-text-us.png +0 -0
- package/dist/consultation-prompt.webp +0 -0
- package/dist/mm-intake-widget.js +320 -102
- package/package.json +14 -11
package/README.md
CHANGED
|
@@ -9,9 +9,34 @@ Load the widget with a script tag, pinning an exact version:
|
|
|
9
9
|
></script>
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Versions
|
|
13
|
+
|
|
14
|
+
semantic-release publishes on every merge that touches this package and picks
|
|
15
|
+
the version from the commit messages, so the `version` in `package.json` is a
|
|
16
|
+
placeholder, don't bump it by hand.
|
|
17
|
+
|
|
18
|
+
| Branch | Version | dist-tag |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| `develop` | `x.y.z-alpha.N` | `alpha` |
|
|
21
|
+
| `release/**` | `x.y.z-beta.N` | `beta` |
|
|
22
|
+
| `main` | `x.y.z` | `latest` |
|
|
23
|
+
|
|
24
|
+
Each bundle also carries its matching chat environment: development for
|
|
25
|
+
`alpha`, UAT for `beta`, and production for `latest`. `data-chat-url` remains
|
|
26
|
+
an explicit override.
|
|
27
|
+
|
|
28
|
+
`fix:` bumps the patch, `feat:` the minor, a `BREAKING CHANGE:` footer the
|
|
29
|
+
major. Every release is tagged `intake-agent-vx.y.z`.
|
|
30
|
+
|
|
12
31
|
## Attributes
|
|
13
32
|
|
|
14
33
|
| Attribute | Default | Purpose |
|
|
15
34
|
| --- | --- | --- |
|
|
16
|
-
| `data-chat-url` |
|
|
35
|
+
| `data-chat-url` | Release environment URL | Chat iframe target |
|
|
17
36
|
| `data-auto-open` | `false` | Open the chat immediately after mount |
|
|
37
|
+
|
|
38
|
+
The consultation modal reads `window.drupalSettings.mxPopup`, falling back to
|
|
39
|
+
`window.mxChatbotSettings.mxPopup` when the Drupal field is absent. Only the
|
|
40
|
+
boolean `false` suppresses the modal, including calls to `showPrompt()`.
|
|
41
|
+
Missing settings preserve the usual display rules. Settings are read when the
|
|
42
|
+
modal is about to appear; the launcher and chat remain available.
|
|
Binary file
|
|
Binary file
|
package/dist/mm-intake-widget.js
CHANGED
|
@@ -1,41 +1,123 @@
|
|
|
1
1
|
(function() {
|
|
2
2
|
"use strict";
|
|
3
|
-
const DEFAULT_CHAT_PATH = "/embed/intake";
|
|
4
3
|
const readLoaderConfig = (script, baseUri) => {
|
|
5
4
|
var _a;
|
|
6
5
|
const scriptUrl = new URL(script.src, baseUri);
|
|
7
|
-
const
|
|
6
|
+
const bundledChatUrl = "https://chat.mx2.dev/embed/intake";
|
|
7
|
+
const chatUrl = new URL((_a = script.dataset.chatUrl) != null ? _a : bundledChatUrl, scriptUrl).href;
|
|
8
8
|
return {
|
|
9
|
+
assetBaseUrl: new URL(".", scriptUrl).href,
|
|
9
10
|
autoOpen: script.dataset.autoOpen === "true",
|
|
10
11
|
chatOrigin: new URL(chatUrl).origin,
|
|
11
12
|
chatUrl
|
|
12
13
|
};
|
|
13
14
|
};
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
const ATTRIBUTION_FIELD_MAX_LENGTH = {
|
|
16
|
+
tracked_landing_page: 4096,
|
|
17
|
+
tracked_landing_timestamp: 255,
|
|
18
|
+
conversion_page: 255,
|
|
19
|
+
ga_client_id: 255,
|
|
20
|
+
gclid_value: 255,
|
|
21
|
+
marketing_source: 255,
|
|
22
|
+
marketing_sub_source: 255
|
|
23
|
+
};
|
|
24
|
+
const ATTRIBUTION_FIELDS = Object.keys(ATTRIBUTION_FIELD_MAX_LENGTH);
|
|
25
|
+
const sanitizeIntakeChatAttribution = (value) => {
|
|
26
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const record = value;
|
|
30
|
+
const sanitized = {};
|
|
31
|
+
for (const field of ATTRIBUTION_FIELDS) {
|
|
32
|
+
const fieldValue = record[field];
|
|
33
|
+
const trimmed = typeof fieldValue === "string" ? fieldValue.trim() : "";
|
|
34
|
+
if (trimmed !== "") {
|
|
35
|
+
sanitized[field] = trimmed.slice(0, ATTRIBUTION_FIELD_MAX_LENGTH[field]);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(sanitized).length === 0 ? null : sanitized;
|
|
39
|
+
};
|
|
40
|
+
const LANDING_PAGE_COOKIES = ["trackedLandingPage", "tracked_landing_page"];
|
|
41
|
+
const LANDING_TIMESTAMP_COOKIES = ["trackedLandingPageTimestamp", "tracked_landing_timestamp"];
|
|
42
|
+
const readTimestampCookie = (cookie) => {
|
|
43
|
+
const raw = readFirstCookie(cookie, LANDING_TIMESTAMP_COOKIES);
|
|
44
|
+
return raw === void 0 ? void 0 : decodeCookieValue(raw);
|
|
45
|
+
};
|
|
46
|
+
const readCookie = (cookie, name) => {
|
|
47
|
+
var _a;
|
|
48
|
+
const raw = (_a = cookie.split(";").map((entry) => entry.trim()).find((entry) => entry.startsWith(`${name}=`))) == null ? void 0 : _a.slice(name.length + 1);
|
|
49
|
+
return raw === void 0 || raw === "" ? void 0 : raw;
|
|
50
|
+
};
|
|
51
|
+
const readFirstCookie = (cookie, names) => {
|
|
52
|
+
for (const name of names) {
|
|
53
|
+
const raw = readCookie(cookie, name);
|
|
54
|
+
if (raw !== void 0) {
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return void 0;
|
|
59
|
+
};
|
|
60
|
+
const decodeCookieValue = (raw) => {
|
|
17
61
|
try {
|
|
18
|
-
return
|
|
62
|
+
return decodeURIComponent(raw);
|
|
19
63
|
} catch {
|
|
20
|
-
return
|
|
64
|
+
return raw;
|
|
21
65
|
}
|
|
22
66
|
};
|
|
23
|
-
const
|
|
67
|
+
const parseUrl = (value) => {
|
|
24
68
|
try {
|
|
25
|
-
|
|
69
|
+
return new URL(value);
|
|
26
70
|
} catch {
|
|
71
|
+
return null;
|
|
27
72
|
}
|
|
28
73
|
};
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (hasUnread) {
|
|
34
|
-
writeFlag(UNREAD_STATE_KEY, true, storage);
|
|
35
|
-
return;
|
|
74
|
+
const readLandingPage = (cookie) => {
|
|
75
|
+
const raw = readFirstCookie(cookie, LANDING_PAGE_COOKIES);
|
|
76
|
+
if (raw === void 0 || parseUrl(raw) !== null) {
|
|
77
|
+
return raw;
|
|
36
78
|
}
|
|
79
|
+
const decoded = decodeCookieValue(raw);
|
|
80
|
+
return parseUrl(decoded) === null ? raw : decoded;
|
|
81
|
+
};
|
|
82
|
+
const readGaClientId = (cookie) => {
|
|
83
|
+
const raw = readCookie(cookie, "_ga");
|
|
84
|
+
if (raw === void 0) {
|
|
85
|
+
return void 0;
|
|
86
|
+
}
|
|
87
|
+
const value = decodeCookieValue(raw);
|
|
88
|
+
const parts = value.split(".");
|
|
89
|
+
return parts.length >= 4 ? parts.slice(-2).join(".") : value;
|
|
90
|
+
};
|
|
91
|
+
const readGclid = (landingUrl) => {
|
|
92
|
+
var _a, _b;
|
|
93
|
+
return landingUrl === void 0 ? void 0 : (_b = (_a = parseUrl(landingUrl)) == null ? void 0 : _a.searchParams.get("gclid")) != null ? _b : void 0;
|
|
94
|
+
};
|
|
95
|
+
const readPathname = (url) => {
|
|
96
|
+
var _a;
|
|
97
|
+
return (_a = parseUrl(url)) == null ? void 0 : _a.pathname;
|
|
98
|
+
};
|
|
99
|
+
const buildHostAttribution = (currentUrl = globalThis.location.href, cookie = document.cookie) => {
|
|
100
|
+
var _a;
|
|
101
|
+
const landingPage = readLandingPage(cookie);
|
|
102
|
+
return (_a = sanitizeIntakeChatAttribution({
|
|
103
|
+
tracked_landing_page: landingPage,
|
|
104
|
+
tracked_landing_timestamp: readTimestampCookie(cookie),
|
|
105
|
+
conversion_page: readPathname(currentUrl),
|
|
106
|
+
ga_client_id: readGaClientId(cookie),
|
|
107
|
+
gclid_value: readGclid(landingPage)
|
|
108
|
+
})) != null ? _a : {};
|
|
109
|
+
};
|
|
110
|
+
const OPEN_STATE_KEY = "mm-intake-widget-open";
|
|
111
|
+
const readOpenState = (storage = sessionStorage) => {
|
|
37
112
|
try {
|
|
38
|
-
storage.
|
|
113
|
+
return storage.getItem(OPEN_STATE_KEY) === "true";
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
const writeOpenState = (isOpen, storage = sessionStorage) => {
|
|
119
|
+
try {
|
|
120
|
+
storage.setItem(OPEN_STATE_KEY, String(isOpen));
|
|
39
121
|
} catch {
|
|
40
122
|
}
|
|
41
123
|
};
|
|
@@ -43,6 +125,9 @@
|
|
|
43
125
|
const EMBED_MESSAGE_TYPE = Object.freeze({
|
|
44
126
|
HOST_READY: "mm:intake:host-ready",
|
|
45
127
|
CHILD_READY: "mm:intake:child-ready",
|
|
128
|
+
// Host asks the embedded chat to put the caret in its composer. Kept
|
|
129
|
+
// separate from HOST_READY so signing-card handshakes never steal focus.
|
|
130
|
+
FOCUS_COMPOSER: "mm:intake:focus-composer",
|
|
46
131
|
SIGNING_REQUESTED: "mm:intake:signing-requested",
|
|
47
132
|
SIGNING_RECORDED: "mm:intake:signing-recorded",
|
|
48
133
|
// Chat asks the host page for a signed-in token; the host answers with
|
|
@@ -60,11 +145,18 @@
|
|
|
60
145
|
// belongs to the focused field, so losing it means the keyboard is already
|
|
61
146
|
// leaving — which the host's own visual viewport only admits ~500ms later.
|
|
62
147
|
CARET_STATE: "mm:intake:caret-state",
|
|
63
|
-
// Chat tells the host an inbound AI or operator message just landed
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// the panel was closed at the time.
|
|
67
|
-
MESSAGE_RECEIVED: "mm:intake:message-received"
|
|
148
|
+
// Chat tells the host an inbound AI or operator message just landed. Kept in
|
|
149
|
+
// the protocol for a future unread affordance; the TEXT US sticky has no badge
|
|
150
|
+
// today. Payload-free: only the chat knows a message arrived while only the
|
|
151
|
+
// host knows whether the panel was closed at the time.
|
|
152
|
+
MESSAGE_RECEIVED: "mm:intake:message-received",
|
|
153
|
+
// Chat asks the host for a fresh attribution snapshot right before a /chat
|
|
154
|
+
// turn; the host answers with ATTRIBUTION as `{ attribution }`. The snapshot
|
|
155
|
+
// sent with HOST_READY is read once at mount and races the marketing site's
|
|
156
|
+
// async first-touch cookie write (MX2-26181), so the landing URL — and the
|
|
157
|
+
// gclid parsed out of it — can only be caught by re-reading at send time.
|
|
158
|
+
GET_ATTRIBUTION: "mm:intake:get-attribution",
|
|
159
|
+
ATTRIBUTION: "mm:intake:attribution"
|
|
68
160
|
});
|
|
69
161
|
const isRecord = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
70
162
|
const isEmbedProtocolMessage = (value, type) => isRecord(value) && value.type === type && value.version === EMBED_PROTOCOL_VERSION && isRecord(value.payload);
|
|
@@ -119,6 +211,13 @@
|
|
|
119
211
|
return null;
|
|
120
212
|
}
|
|
121
213
|
};
|
|
214
|
+
const hasCompletedSigningEvent = (currentUrl = globalThis.location.href) => {
|
|
215
|
+
try {
|
|
216
|
+
return new URL(currentUrl).searchParams.get("event") === SIGNING_COMPLETE_EVENT;
|
|
217
|
+
} catch {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
122
221
|
const LUCIDE_ICON_PATHS = {
|
|
123
222
|
"message-circle": '<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/>',
|
|
124
223
|
"x": '<path d="M18 6 6 18"/><path d="m6 6 12 12"/>',
|
|
@@ -133,84 +232,75 @@
|
|
|
133
232
|
};
|
|
134
233
|
const FULL_PAGE_PANEL_QUERY = "(max-width:600px),(max-height:500px)";
|
|
135
234
|
const lucideIcon = (name, className, strokeWidth = 2) => `<svg class="${className}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="${String(strokeWidth)}" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">${LUCIDE_ICON_PATHS[name]}</svg>`;
|
|
136
|
-
const CHAT_ICON = lucideIcon("message-circle", "mm-icon mm-icon-chat");
|
|
137
235
|
const OPEN_CLOSE_ICON = lucideIcon("x", "mm-icon mm-icon-close");
|
|
236
|
+
const PROMPT_CLOSE_ICON = lucideIcon("x", "mm-prompt-close-icon", 2.5);
|
|
138
237
|
const WIDGET_STYLES = [
|
|
139
238
|
':host{all:initial;color-scheme:light;--mm-yellow:#fdeb01;--mm-yellow-hover:#f5de00;--mm-yellow-pressed:#e6cc00;--mm-blue:#115099;--mm-blue-hover:#0b3568;--mm-ink:#111;--mm-white:#fff;--mm-surface-blue:#f3f8ff;--mm-chrome:#e8eef2;--mm-line:#e2e8f0;--mm-muted:#545454;--mm-green:#0f9d58;--mm-font:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif}',
|
|
140
239
|
"*,*::before,*::after{box-sizing:border-box}",
|
|
141
240
|
"button{font:inherit}",
|
|
142
|
-
"@keyframes mm-launcher-in{from{opacity:0;transform:
|
|
241
|
+
"@keyframes mm-launcher-in{from{opacity:0;transform:translateX(8px) scale(.94)}to{opacity:1;transform:none}}",
|
|
242
|
+
"@keyframes mm-close-in{from{opacity:0;transform:scale(.85)}to{opacity:1;transform:scale(1)}}",
|
|
143
243
|
"@keyframes mm-panel-in{from{opacity:0;transform:translateY(10px) scale(.98)}to{opacity:1;transform:none}}",
|
|
144
244
|
"@keyframes mm-panel-out{from{opacity:1;transform:none}to{opacity:0;transform:translateY(10px) scale(.98)}}",
|
|
145
|
-
"@keyframes mm-
|
|
146
|
-
|
|
147
|
-
// .4 opacity, pulse to full with a 1.2x scale, each dot a beat behind the last.
|
|
245
|
+
"@keyframes mm-prompt-backdrop-in{from{opacity:0}to{opacity:1}}",
|
|
246
|
+
"@keyframes mm-prompt-card-in{from{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}",
|
|
148
247
|
"@keyframes mm-dot{0%,100%{opacity:.4;transform:scale(1)}50%{opacity:1;transform:scale(1.2)}}",
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
|
|
248
|
+
// Closed: LivePerson TEXT US sticky under the chat layer. Open: swap for a
|
|
249
|
+
// blue X disc above the chat so there is always a clear close control.
|
|
250
|
+
// Same asset as forthepeople.com (button-text-us-white-bg), already rotated
|
|
251
|
+
// 90deg CW like GTM. Placement matches #messaging_gbm: top 42%, ~38px thick,
|
|
252
|
+
// no translateY(-50%). right:10px keeps a small inset from the viewport edge.
|
|
253
|
+
// Against the host page the near-max z-index is only the fallback for
|
|
254
|
+
// browsers without the Popover API — everywhere else the mount rides the top
|
|
255
|
+
// layer (widget.ts) and outranks any host z-index.
|
|
256
|
+
".mm-launcher{position:fixed;right:10px;top:42%;z-index:2147482999;display:grid;place-items:center;width:auto;height:auto;padding:0;border:0;border-radius:0;outline:none;background:transparent;color:var(--mm-ink);box-shadow:none;cursor:pointer;user-select:none;-webkit-user-select:none;-webkit-user-drag:none;transform:none;animation:mm-launcher-in .28s cubic-bezier(.23,1,.32,1) both;transition:transform .15s cubic-bezier(.23,1,.32,1),filter .15s ease,background-color .15s ease}",
|
|
152
257
|
".mm-launcher *{user-select:none;-webkit-user-select:none;-webkit-user-drag:none}",
|
|
153
|
-
".mm-launcher:hover{
|
|
154
|
-
".mm-launcher:active{
|
|
258
|
+
".mm-launcher:hover{transform:translateX(-2px);filter:drop-shadow(0 4px 10px rgba(17,17,17,.18))}",
|
|
259
|
+
".mm-launcher:active{transform:none;filter:none}",
|
|
155
260
|
".mm-launcher:focus-visible{outline:3px solid var(--mm-blue);outline-offset:3px}",
|
|
156
|
-
".mm-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
'.mm-launcher[aria-expanded="true"]
|
|
261
|
+
".mm-text-us{display:block;width:38px;height:auto;pointer-events:none}",
|
|
262
|
+
".mm-icon{position:absolute;top:50%;left:50%;display:none;width:22px;height:22px;pointer-events:none;transform:translate(-50%,-50%)}",
|
|
263
|
+
// Open on desktop: X sits on the chat's top-right corner (chat is bottom:24,
|
|
264
|
+
// height min(640px, 100vh - 48px)).
|
|
265
|
+
'.mm-launcher[aria-expanded="true"]{top:calc(100vh - 24px - min(640px,100vh - 48px) + 12px);right:36px;bottom:auto;z-index:2147483001;width:44px;height:44px;border-radius:999px;background:var(--mm-blue);color:var(--mm-white);box-shadow:0 4px 14px rgba(17,17,17,.2);transform:none;animation:mm-close-in .2s cubic-bezier(.23,1,.32,1) both;filter:none}',
|
|
266
|
+
'.mm-launcher[aria-expanded="true"]:hover,.mm-launcher[aria-expanded="true"]:active{transform:none;filter:none;background:var(--mm-blue-hover)}',
|
|
267
|
+
'.mm-launcher[aria-expanded="true"] .mm-text-us{display:none}',
|
|
161
268
|
'.mm-launcher[aria-expanded="true"] .mm-icon-close{display:block}',
|
|
162
|
-
|
|
163
|
-
// tint. Measured on iOS 26: while the panel is full-page, Safari takes the
|
|
164
|
-
// colour of its own status-bar and URL-bar bands straight off this background
|
|
165
|
-
// — ahead of the host page's `theme-color` and root background, which it
|
|
166
|
-
// ignores outright once the chat frame has committed a navigation. So this is
|
|
167
|
-
// the lever that decides what the bands look like, and the bands sit right
|
|
168
|
-
// against the chat's header and trust line.
|
|
169
|
-
// Two flat halves rather than one colour, because the two bands are sampled
|
|
170
|
-
// independently: the top one always sits against the chat header, while the
|
|
171
|
-
// bottom one sits against the trust line or, with a keyboard up, the composer.
|
|
172
|
-
".mm-chat{position:fixed;right:24px;bottom:100px;z-index:2147483000;display:flex;flex-direction:column;width:min(400px,calc(100vw - 32px));height:min(640px,calc(100vh - 132px));overflow:hidden;border:1px solid var(--mm-line);border-radius:20px;background:linear-gradient(to bottom,var(--mm-chrome) 0 50%,var(--mm-panel-bottom,var(--mm-chrome)) 50% 100%);box-shadow:0 5px 25px rgba(0,0,0,.2);animation:mm-panel-in .18s ease-out both;font-family:var(--mm-font)}",
|
|
173
|
-
// Keyboard up, the chat drops its trust line and the composer becomes the
|
|
174
|
-
// bottom-most surface, so the bottom band has to follow the composer. Only
|
|
175
|
-
// the bottom: the header is still up there against the top one.
|
|
269
|
+
".mm-chat{position:fixed;right:24px;bottom:24px;z-index:2147483000;display:flex;flex-direction:column;width:min(400px,calc(100vw - 32px));height:min(640px,calc(100vh - 48px));overflow:hidden;border:1px solid var(--mm-line);border-radius:20px;background:linear-gradient(to bottom,var(--mm-chrome) 0 50%,var(--mm-panel-bottom,var(--mm-chrome)) 50% 100%);box-shadow:0 5px 25px rgba(0,0,0,.2);animation:mm-panel-in .18s ease-out both;font-family:var(--mm-font)}",
|
|
176
270
|
'.mm-chat[data-keyboard="open"]{--mm-panel-bottom:var(--mm-surface-blue)}',
|
|
177
271
|
'.mm-chat[data-state="closing"]{pointer-events:none;animation-name:mm-panel-out}',
|
|
178
272
|
'.mm-chat[data-state="closed"]{visibility:hidden;animation-name:none}',
|
|
179
273
|
".mm-frame-wrap{position:relative;min-height:0;flex:1;background:linear-gradient(to bottom,var(--mm-chrome) 0 50%,var(--mm-panel-bottom,var(--mm-chrome)) 50% 100%)}",
|
|
180
|
-
// A chat tone, never white: the frame paints this in the gap between the
|
|
181
|
-
// document's load event and the app's first frame, and on a phone that gap is
|
|
182
|
-
// the whole screen.
|
|
183
274
|
".mm-frame{display:block;width:100%;height:100%;border:0;background:var(--mm-chrome)}",
|
|
184
275
|
".mm-loading{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:5px;background:var(--mm-chrome)}",
|
|
185
276
|
".mm-dot{width:8px;height:8px;border-radius:999px;background:var(--mm-muted);animation:mm-dot 1.2s ease-in-out infinite}",
|
|
186
277
|
".mm-dot:nth-child(2){animation-delay:.15s}",
|
|
187
278
|
".mm-dot:nth-child(3){animation-delay:.3s}",
|
|
279
|
+
".mm-prompt-backdrop{position:fixed;inset:0;z-index:2147483002;display:grid;place-items:center;padding:16px;overscroll-behavior:contain;touch-action:none;background:rgba(0,0,0,.64);animation:mm-prompt-backdrop-in .25s cubic-bezier(.23,1,.32,1) both;font-family:var(--mm-font)}",
|
|
280
|
+
".mm-prompt-card{position:relative;width:min(632px,calc(100vw - 32px),calc((100vh - 32px)*1.26148));aspect-ratio:632/501;animation:mm-prompt-card-in .25s cubic-bezier(.23,1,.32,1) both}",
|
|
281
|
+
".mm-prompt-image{display:block;width:100%;height:100%;object-fit:contain;pointer-events:none}",
|
|
282
|
+
".mm-prompt-open,.mm-prompt-close{position:absolute;border:0;outline:none;background:transparent;cursor:pointer}",
|
|
283
|
+
".mm-prompt-open{inset:0;border-radius:24px}",
|
|
284
|
+
".mm-prompt-close{top:4.5%;right:3.1%;z-index:1;display:grid;place-items:center;width:44px;height:44px;border-radius:999px;color:var(--mm-white)}",
|
|
285
|
+
'.mm-prompt-close::before{content:"";position:absolute;width:28px;height:28px;border-radius:999px;background:#050505;box-shadow:0 1px 4px rgba(0,0,0,.35);transition:background-color .12s ease}',
|
|
286
|
+
".mm-prompt-close-icon{position:relative;display:block;width:16px;height:16px;pointer-events:none}",
|
|
287
|
+
".mm-prompt-close:hover::before{background:#252525}",
|
|
288
|
+
".mm-prompt-open:focus-visible{outline:3px solid var(--mm-yellow);outline-offset:-8px}",
|
|
289
|
+
".mm-prompt-close:focus-visible{outline:none}",
|
|
290
|
+
".mm-prompt-close:focus-visible::before{background:#050505;box-shadow:inset 0 0 0 2px var(--mm-yellow),0 1px 4px rgba(0,0,0,.35)}",
|
|
188
291
|
"[hidden]{display:none!important}",
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
// `min-height:0` on the panel so a short landscape viewport shrinks it all the
|
|
194
|
-
// way down instead of overflowing the screen and clipping the chat's own
|
|
195
|
-
// rotate-your-phone overlay.
|
|
196
|
-
//
|
|
197
|
-
// 16px from the corner is right for a normal host site, which has nothing of
|
|
198
|
-
// its own down there. A host that mounts its own bottom chrome — the client
|
|
199
|
-
// portal and its tab bar — sets `--mm-launcher-bottom` on any ancestor of the
|
|
200
|
-
// mount (custom properties reach into the shadow tree) to lift the launcher
|
|
201
|
-
// clear of it. Only the closed launcher: open, it moves to the top corner and
|
|
202
|
-
// tracks the viewport instead.
|
|
203
|
-
`@media ${FULL_PAGE_PANEL_QUERY}{.mm-launcher{right:16px;bottom:var(--mm-launcher-bottom,16px)}.mm-launcher[aria-expanded="true"]{top:calc(var(--mm-viewport-top,0px) + 10px);right:10px;bottom:auto;width:44px;height:44px;box-shadow:0 4px 14px rgba(17,17,17,.2)}.mm-launcher[aria-expanded="true"] .mm-icon{width:22px;height:22px}.mm-chat{right:0;top:var(--mm-viewport-top,auto);bottom:0;width:100vw;height:var(--mm-viewport-height,100dvh);min-height:0;border:0;border-radius:0}}`,
|
|
204
|
-
"@media(prefers-reduced-motion:reduce){.mm-launcher,.mm-chat{animation:none;transition:none}.mm-badge::after,.mm-dot{animation:none}}"
|
|
292
|
+
`@media ${FULL_PAGE_PANEL_QUERY}{.mm-launcher[aria-expanded="true"]{top:calc(var(--mm-viewport-top,0px) + 10px);right:10px;bottom:auto;width:44px;height:44px}.mm-chat{left:0;right:0;top:var(--mm-viewport-top,auto);bottom:0;width:auto;height:var(--mm-viewport-height,100dvh);min-height:0;border:0;border-radius:0;box-shadow:0 calc(100lvh - 100svh) 0 var(--mm-panel-bottom,var(--mm-chrome))}}`,
|
|
293
|
+
"@media(forced-colors:active){.mm-prompt-close:focus-visible{outline:2px solid ButtonText;outline-offset:-9px}}",
|
|
294
|
+
"@media(prefers-reduced-motion:reduce){.mm-launcher,.mm-chat{animation:none;transition:none}.mm-prompt-card{animation-name:mm-prompt-backdrop-in}.mm-dot{animation:none}}"
|
|
205
295
|
].join("");
|
|
206
|
-
const
|
|
296
|
+
const widgetMarkup = (launcherImageUrl, promptImageUrl) => [
|
|
207
297
|
`<style>${WIDGET_STYLES}</style>`,
|
|
208
|
-
`<button class="mm-launcher" type="button" aria-label="Open intake chat" aria-expanded="false" draggable="false"
|
|
298
|
+
`<button class="mm-launcher" type="button" aria-label="Open intake chat" aria-expanded="false" draggable="false"><img class="mm-text-us" src="${launcherImageUrl}" alt="" width="38" height="147" decoding="async" />${OPEN_CLOSE_ICON}</button>`,
|
|
209
299
|
'<section class="mm-chat" role="dialog" aria-label="Morgan and Morgan intake chat" inert hidden>',
|
|
210
|
-
// Dots rather than a sentence: the panel is up for a second or two and a line
|
|
211
|
-
// of copy reads like an error the visitor has to act on. The label carries the
|
|
212
|
-
// same information for a screen reader.
|
|
213
300
|
'<div class="mm-frame-wrap"><div class="mm-loading" role="status" aria-label="Loading secure chat"><span class="mm-dot"></span><span class="mm-dot"></span><span class="mm-dot"></span></div><iframe class="mm-frame mm-chat-frame" title="Morgan and Morgan intake chat" referrerpolicy="strict-origin-when-cross-origin"></iframe></div>',
|
|
301
|
+
"</section>",
|
|
302
|
+
'<section class="mm-prompt-backdrop" role="dialog" aria-modal="true" aria-label="Start your free consultation" inert hidden>',
|
|
303
|
+
`<div class="mm-prompt-card"><img class="mm-prompt-image" src="${promptImageUrl}" alt="Our team is available 24/7. Chat with a real person. Click to start your free consultation." width="632" height="501" decoding="async" /><button class="mm-prompt-open" type="button" aria-label="Message us about a free consultation"></button><button class="mm-prompt-close" type="button" aria-label="Dismiss consultation prompt">${PROMPT_CLOSE_ICON}</button></div>`,
|
|
214
304
|
"</section>"
|
|
215
305
|
].join("");
|
|
216
306
|
const requireElement = (shadow, selector) => {
|
|
@@ -220,56 +310,105 @@
|
|
|
220
310
|
}
|
|
221
311
|
return element;
|
|
222
312
|
};
|
|
223
|
-
const renderWidgetShell = (shadow) => {
|
|
224
|
-
shadow.innerHTML =
|
|
313
|
+
const renderWidgetShell = (shadow, launcherImageUrl, promptImageUrl) => {
|
|
314
|
+
shadow.innerHTML = widgetMarkup(launcherImageUrl, promptImageUrl);
|
|
225
315
|
return {
|
|
226
316
|
chatDialog: requireElement(shadow, ".mm-chat"),
|
|
227
317
|
chatFrame: requireElement(shadow, ".mm-chat-frame"),
|
|
228
318
|
chatLoading: requireElement(shadow, ".mm-loading"),
|
|
229
319
|
launcher: requireElement(shadow, ".mm-launcher"),
|
|
230
|
-
|
|
320
|
+
promptBackdrop: requireElement(shadow, ".mm-prompt-backdrop"),
|
|
321
|
+
promptClose: requireElement(shadow, ".mm-prompt-close"),
|
|
322
|
+
promptOpen: requireElement(shadow, ".mm-prompt-open")
|
|
231
323
|
};
|
|
232
324
|
};
|
|
233
325
|
const PANEL_ANIMATION_MS = 180;
|
|
326
|
+
const PROMPT_DELAY_MS = 5e3;
|
|
327
|
+
const PROMPT_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
|
|
328
|
+
const PROMPT_COOLDOWN_KEY = "mm-intake-widget-prompt-dismissed-at";
|
|
329
|
+
const PROMPT_SESSION_KEY = "mm-intake-widget-prompt-dismissed";
|
|
234
330
|
const CHAT_CHROME_COLOR = "#e8eef2";
|
|
235
331
|
const KEYBOARD_MIN_SHRINK_PX = 100;
|
|
236
332
|
const CHAT_READY_FALLBACK_MS = 4e3;
|
|
237
|
-
const getLauncherLabel = (isOpen, hasUnread) => {
|
|
238
|
-
if (isOpen) {
|
|
239
|
-
return "Close intake chat";
|
|
240
|
-
}
|
|
241
|
-
return hasUnread ? "Open intake chat, new message" : "Open intake chat";
|
|
242
|
-
};
|
|
243
333
|
const cleanSigningReturnUrl = () => {
|
|
244
334
|
const cleanedReturnUrl = buildSigningReturnUrl();
|
|
245
335
|
if (cleanedReturnUrl !== null) {
|
|
246
336
|
globalThis.history.replaceState(globalThis.history.state, "", cleanedReturnUrl);
|
|
247
337
|
}
|
|
248
338
|
};
|
|
339
|
+
const hasPromptCooldown = () => {
|
|
340
|
+
try {
|
|
341
|
+
if (globalThis.sessionStorage.getItem(PROMPT_SESSION_KEY) === "true") {
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
const dismissedAt = Number(globalThis.localStorage.getItem(PROMPT_COOLDOWN_KEY));
|
|
348
|
+
return Number.isFinite(dismissedAt) && dismissedAt > 0 && Date.now() - dismissedAt < PROMPT_COOLDOWN_MS;
|
|
349
|
+
} catch {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
const rememberPromptDismissal = () => {
|
|
354
|
+
try {
|
|
355
|
+
globalThis.sessionStorage.setItem(PROMPT_SESSION_KEY, "true");
|
|
356
|
+
} catch {
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
globalThis.localStorage.setItem(PROMPT_COOLDOWN_KEY, String(Date.now()));
|
|
360
|
+
} catch {
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const playPromptSound = async () => {
|
|
364
|
+
try {
|
|
365
|
+
const context = new AudioContext();
|
|
366
|
+
const oscillator = context.createOscillator();
|
|
367
|
+
const gain = context.createGain();
|
|
368
|
+
oscillator.frequency.setValueAtTime(660, context.currentTime);
|
|
369
|
+
oscillator.frequency.exponentialRampToValueAtTime(880, context.currentTime + 0.16);
|
|
370
|
+
gain.gain.setValueAtTime(0.08, context.currentTime);
|
|
371
|
+
gain.gain.exponentialRampToValueAtTime(1e-3, context.currentTime + 0.2);
|
|
372
|
+
oscillator.connect(gain);
|
|
373
|
+
gain.connect(context.destination);
|
|
374
|
+
oscillator.start();
|
|
375
|
+
oscillator.stop(context.currentTime + 0.2);
|
|
376
|
+
oscillator.addEventListener("ended", () => void context.close());
|
|
377
|
+
await context.resume();
|
|
378
|
+
} catch {
|
|
379
|
+
}
|
|
380
|
+
};
|
|
249
381
|
const mountIntakeWidget = (config, onDestroy) => {
|
|
250
382
|
var _a;
|
|
251
383
|
const mount = document.createElement("div");
|
|
252
384
|
mount.dataset.mmIntakeWidget = "";
|
|
253
385
|
const shadow = mount.attachShadow({ mode: "open" });
|
|
254
|
-
const
|
|
386
|
+
const assetUrl = (fileName) => {
|
|
387
|
+
var _a2;
|
|
388
|
+
return new URL(fileName, (_a2 = config.assetBaseUrl) != null ? _a2 : `${config.chatOrigin}/intake-widget/`).href;
|
|
389
|
+
};
|
|
390
|
+
const elements = renderWidgetShell(shadow, assetUrl("button-text-us.png"), assetUrl("consultation-prompt.webp"));
|
|
255
391
|
document.body.append(mount);
|
|
256
|
-
|
|
392
|
+
if (typeof mount.showPopover === "function") {
|
|
393
|
+
mount.setAttribute("popover", "manual");
|
|
394
|
+
mount.style.cssText = "position:fixed;inset:0 auto auto 0;width:0;height:0;margin:0;border:0;padding:0;overflow:visible;background:transparent";
|
|
395
|
+
try {
|
|
396
|
+
mount.showPopover();
|
|
397
|
+
} catch {
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const returnedFromSigning = hasCompletedSigningEvent();
|
|
257
401
|
let chatHasLoaded = false;
|
|
258
402
|
let chatCloseTimer;
|
|
259
403
|
let chatReadyTimer;
|
|
404
|
+
let promptTimer;
|
|
405
|
+
let promptReturnFocus = null;
|
|
260
406
|
let isKeyboardOpen = false;
|
|
261
407
|
let hasReleasedKeyboardEarly = false;
|
|
262
|
-
let hasUnread = readUnreadState();
|
|
263
|
-
const setHasUnread = (next) => {
|
|
264
|
-
hasUnread = next;
|
|
265
|
-
elements.unreadBadge.hidden = !next;
|
|
266
|
-
writeUnreadState(next);
|
|
267
|
-
syncLauncherLabel();
|
|
268
|
-
};
|
|
269
408
|
const syncLauncherLabel = () => {
|
|
270
409
|
const isOpen = elements.chatDialog.dataset.state === "open";
|
|
271
410
|
elements.launcher.setAttribute("aria-expanded", String(isOpen));
|
|
272
|
-
elements.launcher.setAttribute("aria-label",
|
|
411
|
+
elements.launcher.setAttribute("aria-label", isOpen ? "Close intake chat" : "Open intake chat");
|
|
273
412
|
};
|
|
274
413
|
const postToChat = (message) => {
|
|
275
414
|
var _a2;
|
|
@@ -281,11 +420,25 @@
|
|
|
281
420
|
const hostUrl = buildSigningReturnUrl(currentUrl);
|
|
282
421
|
postToChat(
|
|
283
422
|
createEmbedProtocolMessage(EMBED_MESSAGE_TYPE.HOST_READY, {
|
|
423
|
+
// Marketing attribution (MX2-25162): the marketing site's first-touch
|
|
424
|
+
// cookies and the page the chat opened on are only visible from this
|
|
425
|
+
// page, never from the cross-origin chat frame. The chat forwards them
|
|
426
|
+
// on its first /chat turn and Salesforce builds the intake's
|
|
427
|
+
// Marketing_Tracking_Info__c record from them.
|
|
428
|
+
attribution: buildHostAttribution(),
|
|
284
429
|
...hostUrl === null ? {} : { hostUrl },
|
|
285
|
-
...completedEnvelopeId === null ? {} : { completedEnvelopeId }
|
|
430
|
+
...completedEnvelopeId === null ? {} : { completedEnvelopeId },
|
|
431
|
+
// Sent even without an envelope id: the Salesforce service is what
|
|
432
|
+
// appends `envelope_id` to the return URL, and the prod org has shipped
|
|
433
|
+
// without that append. The chat's signing card falls back to its own
|
|
434
|
+
// envelope id on this flag.
|
|
435
|
+
...hasCompletedSigningEvent(currentUrl) ? { signingComplete: true } : {}
|
|
286
436
|
})
|
|
287
437
|
);
|
|
288
438
|
};
|
|
439
|
+
const focusChatComposer = () => {
|
|
440
|
+
postToChat(createEmbedProtocolMessage(EMBED_MESSAGE_TYPE.FOCUS_COMPOSER));
|
|
441
|
+
};
|
|
289
442
|
const answerAuthTokenRequest = async () => {
|
|
290
443
|
var _a2;
|
|
291
444
|
let token;
|
|
@@ -308,6 +461,34 @@
|
|
|
308
461
|
chatReadyTimer = void 0;
|
|
309
462
|
elements.chatLoading.hidden = true;
|
|
310
463
|
};
|
|
464
|
+
const dismissPrompt = (returnFocus = false) => {
|
|
465
|
+
globalThis.clearTimeout(promptTimer);
|
|
466
|
+
promptTimer = void 0;
|
|
467
|
+
const wasVisible = !elements.promptBackdrop.hidden;
|
|
468
|
+
elements.promptBackdrop.hidden = true;
|
|
469
|
+
elements.promptBackdrop.setAttribute("inert", "");
|
|
470
|
+
rememberPromptDismissal();
|
|
471
|
+
if (returnFocus && wasVisible) {
|
|
472
|
+
const focusTarget = (promptReturnFocus == null ? void 0 : promptReturnFocus.isConnected) === true ? promptReturnFocus : elements.launcher;
|
|
473
|
+
promptReturnFocus = null;
|
|
474
|
+
focusTarget.focus();
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
const showPrompt = () => {
|
|
478
|
+
var _a2, _b, _c;
|
|
479
|
+
globalThis.clearTimeout(promptTimer);
|
|
480
|
+
promptTimer = void 0;
|
|
481
|
+
const host = globalThis;
|
|
482
|
+
const mxPopup = (_c = (_a2 = host.drupalSettings) == null ? void 0 : _a2.mxPopup) != null ? _c : (_b = host.mxChatbotSettings) == null ? void 0 : _b.mxPopup;
|
|
483
|
+
if (mxPopup === false || hasPromptCooldown() || elements.chatDialog.dataset.state === "open") {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
promptReturnFocus = document.activeElement instanceof HTMLElement && document.activeElement !== mount ? document.activeElement : null;
|
|
487
|
+
elements.promptBackdrop.hidden = false;
|
|
488
|
+
elements.promptBackdrop.removeAttribute("inert");
|
|
489
|
+
elements.promptClose.focus();
|
|
490
|
+
void playPromptSound();
|
|
491
|
+
};
|
|
311
492
|
const syncViewport = () => {
|
|
312
493
|
const viewport = globalThis.visualViewport;
|
|
313
494
|
if (!viewport || viewport.height === 0) {
|
|
@@ -338,9 +519,11 @@
|
|
|
338
519
|
let createdThemeColor = null;
|
|
339
520
|
const hostRoots = [document.documentElement, document.body];
|
|
340
521
|
let hostBackgroundColorBackup = null;
|
|
522
|
+
let hostOverflowBackup = null;
|
|
341
523
|
const borrowHostChrome = () => {
|
|
342
524
|
if (themeColorBackup === null) {
|
|
343
525
|
hostBackgroundColorBackup = hostRoots.map((root) => root.style.backgroundColor);
|
|
526
|
+
hostOverflowBackup = hostRoots.map((root) => root.style.overflow);
|
|
344
527
|
const metas = [...document.head.querySelectorAll('meta[name="theme-color"]')];
|
|
345
528
|
if (metas.length === 0) {
|
|
346
529
|
createdThemeColor = document.createElement("meta");
|
|
@@ -352,13 +535,14 @@
|
|
|
352
535
|
}
|
|
353
536
|
for (const root of hostRoots) {
|
|
354
537
|
root.style.setProperty("background-color", CHAT_CHROME_COLOR, "important");
|
|
538
|
+
root.style.setProperty("overflow", "hidden", "important");
|
|
355
539
|
}
|
|
356
540
|
for (const { meta } of themeColorBackup) {
|
|
357
541
|
meta.content = CHAT_CHROME_COLOR;
|
|
358
542
|
}
|
|
359
543
|
};
|
|
360
544
|
const restoreHostChrome = () => {
|
|
361
|
-
var _a2;
|
|
545
|
+
var _a2, _b;
|
|
362
546
|
if (themeColorBackup === null) {
|
|
363
547
|
return;
|
|
364
548
|
}
|
|
@@ -370,8 +554,10 @@
|
|
|
370
554
|
themeColorBackup = null;
|
|
371
555
|
for (const [index, root] of hostRoots.entries()) {
|
|
372
556
|
root.style.backgroundColor = (_a2 = hostBackgroundColorBackup == null ? void 0 : hostBackgroundColorBackup[index]) != null ? _a2 : "";
|
|
557
|
+
root.style.overflow = (_b = hostOverflowBackup == null ? void 0 : hostOverflowBackup[index]) != null ? _b : "";
|
|
373
558
|
}
|
|
374
559
|
hostBackgroundColorBackup = null;
|
|
560
|
+
hostOverflowBackup = null;
|
|
375
561
|
};
|
|
376
562
|
const syncHostChrome = () => {
|
|
377
563
|
if (elements.chatDialog.dataset.state !== "open") {
|
|
@@ -421,19 +607,21 @@
|
|
|
421
607
|
stopViewportTracking();
|
|
422
608
|
};
|
|
423
609
|
const openChat = () => {
|
|
610
|
+
dismissPrompt();
|
|
424
611
|
globalThis.clearTimeout(chatCloseTimer);
|
|
425
612
|
chatCloseTimer = void 0;
|
|
426
613
|
elements.chatDialog.hidden = false;
|
|
427
614
|
elements.chatDialog.dataset.state = "open";
|
|
428
615
|
elements.chatDialog.removeAttribute("aria-hidden");
|
|
429
616
|
elements.chatDialog.removeAttribute("inert");
|
|
430
|
-
|
|
617
|
+
syncLauncherLabel();
|
|
431
618
|
startViewportTracking();
|
|
432
619
|
ensureChatLoaded();
|
|
433
620
|
elements.chatFrame.focus();
|
|
434
621
|
writeOpenState(true);
|
|
435
622
|
if (chatHasLoaded) {
|
|
436
623
|
postHostReady();
|
|
624
|
+
focusChatComposer();
|
|
437
625
|
}
|
|
438
626
|
};
|
|
439
627
|
const closeChat = () => {
|
|
@@ -469,12 +657,19 @@
|
|
|
469
657
|
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.CHILD_READY)) {
|
|
470
658
|
revealChat();
|
|
471
659
|
postHostReady();
|
|
660
|
+
if (elements.chatDialog.dataset.state === "open") {
|
|
661
|
+
focusChatComposer();
|
|
662
|
+
}
|
|
472
663
|
return;
|
|
473
664
|
}
|
|
474
665
|
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.AUTH_TOKEN_REQUEST)) {
|
|
475
666
|
void answerAuthTokenRequest();
|
|
476
667
|
return;
|
|
477
668
|
}
|
|
669
|
+
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.GET_ATTRIBUTION)) {
|
|
670
|
+
postToChat(createEmbedProtocolMessage(EMBED_MESSAGE_TYPE.ATTRIBUTION, { attribution: buildHostAttribution() }));
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
478
673
|
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.CARET_STATE)) {
|
|
479
674
|
if (message.payload.isFocused === true) {
|
|
480
675
|
hasReleasedKeyboardEarly = false;
|
|
@@ -484,9 +679,6 @@
|
|
|
484
679
|
return;
|
|
485
680
|
}
|
|
486
681
|
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.MESSAGE_RECEIVED)) {
|
|
487
|
-
if (elements.chatDialog.dataset.state !== "open") {
|
|
488
|
-
setHasUnread(true);
|
|
489
|
-
}
|
|
490
682
|
return;
|
|
491
683
|
}
|
|
492
684
|
if (isEmbedProtocolMessage(message, EMBED_MESSAGE_TYPE.SIGNING_REQUESTED)) {
|
|
@@ -499,9 +691,24 @@
|
|
|
499
691
|
}
|
|
500
692
|
};
|
|
501
693
|
const handleKeydown = (event) => {
|
|
694
|
+
if (event.key === "Tab" && !elements.promptBackdrop.hidden) {
|
|
695
|
+
const { activeElement } = shadow;
|
|
696
|
+
if (!event.shiftKey && activeElement === elements.promptClose) {
|
|
697
|
+
event.preventDefault();
|
|
698
|
+
elements.promptOpen.focus();
|
|
699
|
+
} else if (event.shiftKey && activeElement === elements.promptOpen) {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
elements.promptClose.focus();
|
|
702
|
+
}
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
502
705
|
if (event.key !== "Escape") {
|
|
503
706
|
return;
|
|
504
707
|
}
|
|
708
|
+
if (!elements.promptBackdrop.hidden) {
|
|
709
|
+
dismissPrompt(true);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
505
712
|
if (elements.chatDialog.dataset.state === "open") {
|
|
506
713
|
closeChat();
|
|
507
714
|
}
|
|
@@ -510,6 +717,7 @@
|
|
|
510
717
|
stopViewportTracking();
|
|
511
718
|
globalThis.clearTimeout(chatCloseTimer);
|
|
512
719
|
globalThis.clearTimeout(chatReadyTimer);
|
|
720
|
+
globalThis.clearTimeout(promptTimer);
|
|
513
721
|
globalThis.removeEventListener("message", handleMessage);
|
|
514
722
|
document.removeEventListener("keydown", handleKeydown);
|
|
515
723
|
mount.remove();
|
|
@@ -522,6 +730,13 @@
|
|
|
522
730
|
openChat();
|
|
523
731
|
}
|
|
524
732
|
});
|
|
733
|
+
elements.promptOpen.addEventListener("click", openChat);
|
|
734
|
+
elements.promptClose.addEventListener("click", () => dismissPrompt(true));
|
|
735
|
+
elements.promptBackdrop.addEventListener("click", (event) => {
|
|
736
|
+
if (event.target === elements.promptBackdrop) {
|
|
737
|
+
dismissPrompt(true);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
525
740
|
elements.chatDialog.addEventListener("animationend", (event) => {
|
|
526
741
|
if (event.target === elements.chatDialog) {
|
|
527
742
|
finishChatClose();
|
|
@@ -535,14 +750,17 @@
|
|
|
535
750
|
});
|
|
536
751
|
globalThis.addEventListener("message", handleMessage);
|
|
537
752
|
document.addEventListener("keydown", handleKeydown);
|
|
538
|
-
|
|
539
|
-
if (
|
|
753
|
+
syncLauncherLabel();
|
|
754
|
+
if (returnedFromSigning || readOpenState()) {
|
|
540
755
|
openChat();
|
|
756
|
+
} else if (!hasPromptCooldown()) {
|
|
757
|
+
promptTimer = globalThis.setTimeout(showPrompt, PROMPT_DELAY_MS);
|
|
541
758
|
}
|
|
542
759
|
return Object.freeze({
|
|
543
760
|
close: closeChat,
|
|
544
761
|
destroy,
|
|
545
762
|
open: openChat,
|
|
763
|
+
showPrompt,
|
|
546
764
|
version: EMBED_PROTOCOL_VERSION
|
|
547
765
|
});
|
|
548
766
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morgan-and-morgan/intake-agent",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0-alpha.10",
|
|
4
4
|
"description": "Embeddable chat widget loader.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"files": [
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"exports": {
|
|
12
12
|
".": "./dist/mm-intake-widget.js",
|
|
13
13
|
"./widget": "./src/widget.ts",
|
|
14
|
+
"./attribution": "./src/attribution.ts",
|
|
14
15
|
"./protocol": "./src/protocol.ts",
|
|
15
16
|
"./signing": "./src/signing.ts",
|
|
16
17
|
"./loader-config": "./src/loader-config.ts"
|
|
@@ -18,20 +19,22 @@
|
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
|
20
21
|
},
|
|
21
|
-
"devDependencies": {
|
|
22
|
-
"eslint": "^9.39.1",
|
|
23
|
-
"lucide-react-native": "^0.562.0",
|
|
24
|
-
"typescript": "~5.9.3",
|
|
25
|
-
"vite": "^7.3.6",
|
|
26
|
-
"@mmcp/eslint-config": "0.1.0",
|
|
27
|
-
"@mmcp/tsconfig": "0.1.0"
|
|
28
|
-
},
|
|
29
22
|
"scripts": {
|
|
30
23
|
"build": "pnpm run build:bundle",
|
|
31
24
|
"build:bundle": "node scripts/generate-icons.mjs && vite build",
|
|
32
25
|
"generate:icons": "node scripts/generate-icons.mjs",
|
|
26
|
+
"test": "node --test 'src/*.test.mjs'",
|
|
33
27
|
"typecheck": "tsc --noEmit",
|
|
34
28
|
"lint": "eslint \"src/**/*.ts\" --quiet",
|
|
35
|
-
"lint:fix": "eslint \"src/**/*.ts\" --quiet --fix"
|
|
29
|
+
"lint:fix": "eslint \"src/**/*.ts\" --quiet --fix",
|
|
30
|
+
"prepublishOnly": "pnpm run build:bundle"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@mmcp/eslint-config": "workspace:*",
|
|
34
|
+
"@mmcp/tsconfig": "workspace:*",
|
|
35
|
+
"eslint": "^9.39.1",
|
|
36
|
+
"lucide-react-native": "^0.562.0",
|
|
37
|
+
"typescript": "~5.9.3",
|
|
38
|
+
"vite": "^7.3.6"
|
|
36
39
|
}
|
|
37
|
-
}
|
|
40
|
+
}
|