@notix-hub/sdk 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture.d.ts +2 -2
- package/dist/index.cjs +96 -46
- package/dist/index.mjs +96 -46
- package/dist/notify.min.js +1 -1
- package/dist/notix.d.ts +1 -0
- package/dist/types.d.ts +12 -1
- package/package.json +1 -1
package/dist/capture.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { NotifyPayload } from './types';
|
|
1
|
+
import type { NotifyPayload, NotixResponse } from './types';
|
|
2
2
|
interface FormMeta {
|
|
3
3
|
title?: string;
|
|
4
4
|
fieldsCount: number;
|
|
5
5
|
}
|
|
6
6
|
type OnSubmit = (payload: NotifyPayload, meta: FormMeta) => void;
|
|
7
|
-
export declare function initFormCapture(onSubmit: OnSubmit, root: Element | Document, debug: boolean): void;
|
|
7
|
+
export declare function initFormCapture(onSubmit: OnSubmit, root: Element | Document, debug: boolean, onCapture?: (response: NotixResponse, payload: NotifyPayload) => void): void;
|
|
8
8
|
export declare function destroyFormCapture(): void;
|
|
9
9
|
export {};
|
package/dist/index.cjs
CHANGED
|
@@ -13,6 +13,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
|
|
|
13
13
|
...(payload.type !== undefined && { notification_type: payload.type }),
|
|
14
14
|
...(payload.priority !== undefined && { priority: payload.priority }),
|
|
15
15
|
...(payload.tag !== undefined && { tag: payload.tag }),
|
|
16
|
+
...(payload.tags !== undefined && { tags: payload.tags }),
|
|
16
17
|
...(payload.payload !== undefined && { payload: payload.payload }),
|
|
17
18
|
};
|
|
18
19
|
const controller = new AbortController();
|
|
@@ -27,6 +28,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
|
|
|
27
28
|
},
|
|
28
29
|
body: JSON.stringify(body),
|
|
29
30
|
signal: controller.signal,
|
|
31
|
+
keepalive: true,
|
|
30
32
|
})
|
|
31
33
|
.then(async (res) => {
|
|
32
34
|
clearTimeout(timer);
|
|
@@ -73,17 +75,17 @@ function xhrSend(endpoint, token, body, timeout) {
|
|
|
73
75
|
|
|
74
76
|
const SUBMIT_DEBOUNCE_MS = 500;
|
|
75
77
|
const pendingForms = new WeakMap();
|
|
76
|
-
function initFormCapture(onSubmit, root, debug) {
|
|
77
|
-
const forms = root.querySelectorAll('form[data-notify]');
|
|
78
|
+
function initFormCapture(onSubmit, root, debug, onCapture) {
|
|
79
|
+
const forms = root.querySelectorAll('form[data-notix], form[data-notify]');
|
|
78
80
|
forms.forEach((form) => {
|
|
79
81
|
if (form.dataset.notixBound)
|
|
80
82
|
return;
|
|
81
83
|
form.dataset.notixBound = '1';
|
|
82
84
|
form.addEventListener('submit', (e) => {
|
|
85
|
+
e.preventDefault();
|
|
83
86
|
const now = Date.now();
|
|
84
87
|
const last = pendingForms.get(form) ?? 0;
|
|
85
88
|
if (now - last < SUBMIT_DEBOUNCE_MS) {
|
|
86
|
-
e.preventDefault();
|
|
87
89
|
return;
|
|
88
90
|
}
|
|
89
91
|
pendingForms.set(form, now);
|
|
@@ -92,10 +94,19 @@ function initFormCapture(onSubmit, root, debug) {
|
|
|
92
94
|
if (debug) {
|
|
93
95
|
console.log('[Notix] Form captured:', payload.title);
|
|
94
96
|
}
|
|
95
|
-
onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(
|
|
97
|
+
onSubmit(payload, { title: payload.title, fieldsCount: payload.payload ? Object.keys(payload.payload).length : 0 });
|
|
98
|
+
// Per-form callback: data-notix-onsuccess="funcName"
|
|
99
|
+
const callbackName = attr(form, 'onsuccess');
|
|
100
|
+
if (callbackName) {
|
|
101
|
+
const fn = window[callbackName];
|
|
102
|
+
if (typeof fn === 'function') {
|
|
103
|
+
// The actual callback fires after notify() resolves — handled in notix.ts
|
|
104
|
+
form.dataset.notixCallback = callbackName;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (onCapture) ;
|
|
96
108
|
}
|
|
97
109
|
catch (err) {
|
|
98
|
-
// don't block form submission
|
|
99
110
|
if (debug) {
|
|
100
111
|
console.warn('[Notix] Form capture failed:', err);
|
|
101
112
|
}
|
|
@@ -106,64 +117,91 @@ function initFormCapture(onSubmit, root, debug) {
|
|
|
106
117
|
function destroyFormCapture() {
|
|
107
118
|
document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
|
|
108
119
|
delete form.dataset.notixBound;
|
|
109
|
-
// listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
|
|
110
120
|
});
|
|
111
121
|
}
|
|
122
|
+
function attr(form, name) {
|
|
123
|
+
const camel = name.charAt(0).toUpperCase() + name.slice(1);
|
|
124
|
+
return form.dataset[`notix${camel}`] ?? form.dataset[`notify${camel}`];
|
|
125
|
+
}
|
|
126
|
+
function elLabel(el) {
|
|
127
|
+
return el.getAttribute('data-notix-label') ?? el.getAttribute('data-notify-label') ?? el.name;
|
|
128
|
+
}
|
|
112
129
|
function extractPayload(form) {
|
|
113
|
-
const title = form
|
|
114
|
-
const
|
|
115
|
-
const tag = form
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.filter(Boolean);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
...(priority && { priority: priority }),
|
|
130
|
-
};
|
|
130
|
+
const title = attr(form, 'title') ?? document.title;
|
|
131
|
+
const typeSlug = attr(form, 'type');
|
|
132
|
+
const tag = attr(form, 'tag');
|
|
133
|
+
const tagsStr = attr(form, 'tags');
|
|
134
|
+
const priority = attr(form, 'priority');
|
|
135
|
+
const bodyTemplate = attr(form, 'body');
|
|
136
|
+
const fieldsStr = attr(form, 'fields');
|
|
137
|
+
const allowedNames = new Set();
|
|
138
|
+
if (fieldsStr) {
|
|
139
|
+
fieldsStr.split(',').map(s => s.trim()).filter(Boolean).forEach(name => allowedNames.add(name));
|
|
140
|
+
}
|
|
141
|
+
if (bodyTemplate) {
|
|
142
|
+
const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
|
|
143
|
+
for (const m of matches) {
|
|
144
|
+
allowedNames.add(m[1]);
|
|
145
|
+
}
|
|
131
146
|
}
|
|
147
|
+
let tags;
|
|
148
|
+
if (tag) {
|
|
149
|
+
tags = [tag];
|
|
150
|
+
}
|
|
151
|
+
if (tagsStr) {
|
|
152
|
+
try {
|
|
153
|
+
const parsed = JSON.parse(tagsStr);
|
|
154
|
+
tags = [...(tags || []), ...(Array.isArray(parsed) ? parsed : [parsed])];
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
tags = tags || [];
|
|
158
|
+
tags.push(tagsStr);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const fieldValues = {};
|
|
132
162
|
const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
|
|
133
|
-
const fieldMap = new Map();
|
|
134
163
|
elements.forEach((el) => {
|
|
135
164
|
if (!el.name)
|
|
136
165
|
return;
|
|
166
|
+
if (!allowedNames.has(el.name))
|
|
167
|
+
return;
|
|
137
168
|
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
138
169
|
if (!el.checked)
|
|
139
170
|
return;
|
|
140
171
|
}
|
|
141
|
-
|
|
142
|
-
return;
|
|
143
|
-
const isMarked = el.hasAttribute('data-notify-field');
|
|
144
|
-
if (specifiedFields || isMarked || (!specifiedFields && !template)) {
|
|
145
|
-
const label = el.getAttribute('data-notify-label') ?? el.name;
|
|
146
|
-
fieldMap.set(label, el.value || '');
|
|
147
|
-
}
|
|
172
|
+
fieldValues[el.name] = el.value || '';
|
|
148
173
|
});
|
|
149
174
|
let body;
|
|
150
|
-
if (
|
|
151
|
-
body =
|
|
152
|
-
if (fieldMap.has(key))
|
|
153
|
-
return fieldMap.get(key);
|
|
154
|
-
const el = form.elements.namedItem(key);
|
|
155
|
-
return el?.value || `{${key}}`;
|
|
156
|
-
});
|
|
175
|
+
if (bodyTemplate) {
|
|
176
|
+
body = bodyTemplate.replace(/\{(\w+)\}/g, (_, fieldName) => fieldValues[fieldName] ?? `{${fieldName}}`);
|
|
157
177
|
}
|
|
158
|
-
|
|
159
|
-
|
|
178
|
+
const payloadData = {};
|
|
179
|
+
elements.forEach((el) => {
|
|
180
|
+
if (!el.name)
|
|
181
|
+
return;
|
|
182
|
+
if (!allowedNames.has(el.name))
|
|
183
|
+
return;
|
|
184
|
+
if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked)
|
|
185
|
+
return;
|
|
186
|
+
payloadData[elLabel(el)] = el.value || '';
|
|
187
|
+
});
|
|
188
|
+
let extraPayload = {};
|
|
189
|
+
const payloadJson = attr(form, 'payload');
|
|
190
|
+
if (payloadJson) {
|
|
191
|
+
try {
|
|
192
|
+
extraPayload = JSON.parse(payloadJson);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
// ignore invalid JSON
|
|
196
|
+
}
|
|
160
197
|
}
|
|
161
198
|
return {
|
|
162
199
|
title,
|
|
163
200
|
...(body && { body }),
|
|
164
|
-
...(
|
|
165
|
-
...(
|
|
201
|
+
...(typeSlug && { type: typeSlug }),
|
|
202
|
+
...(tags && tags.length > 0 && { tags }),
|
|
166
203
|
...(priority && { priority: priority }),
|
|
204
|
+
payload: { ...payloadData, ...extraPayload },
|
|
167
205
|
};
|
|
168
206
|
}
|
|
169
207
|
|
|
@@ -298,7 +336,8 @@ class Notix {
|
|
|
298
336
|
this.timeout = config.timeout ?? 10000;
|
|
299
337
|
this.debug = config.debug ?? false;
|
|
300
338
|
this.metrika = createMetrikaTracker(config.metrika);
|
|
301
|
-
|
|
339
|
+
this.onCapture = config.onCapture;
|
|
340
|
+
if (config.autoCapture === true) {
|
|
302
341
|
this.capture();
|
|
303
342
|
}
|
|
304
343
|
if (config.metricEnabled) {
|
|
@@ -330,8 +369,19 @@ class Notix {
|
|
|
330
369
|
if (this.captureActive)
|
|
331
370
|
return;
|
|
332
371
|
initFormCapture((payload, meta) => {
|
|
333
|
-
this.notify(payload).then(() => {
|
|
372
|
+
this.notify(payload).then((response) => {
|
|
334
373
|
this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
|
|
374
|
+
// Per-form callback from data-notix-onsuccess
|
|
375
|
+
const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
|
|
376
|
+
if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
|
|
377
|
+
const fn = window[form.dataset.notixCallback];
|
|
378
|
+
if (typeof fn === 'function') {
|
|
379
|
+
fn(response);
|
|
380
|
+
}
|
|
381
|
+
delete form.dataset.notixCallback;
|
|
382
|
+
}
|
|
383
|
+
// Global callback from config
|
|
384
|
+
this.onCapture?.(response, payload);
|
|
335
385
|
}).catch(() => { });
|
|
336
386
|
}, root, this.debug);
|
|
337
387
|
this.captureActive = true;
|
|
@@ -400,7 +450,7 @@ if (typeof document !== 'undefined') {
|
|
|
400
450
|
const token = thisScript.dataset.token;
|
|
401
451
|
if (token) {
|
|
402
452
|
const endpoint = thisScript.dataset.endpoint;
|
|
403
|
-
const autoCapture = thisScript.dataset.autoCapture
|
|
453
|
+
const autoCapture = thisScript.dataset.autoCapture === 'true';
|
|
404
454
|
const debug = thisScript.dataset.debug === 'true';
|
|
405
455
|
const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
|
|
406
456
|
const ready = document.readyState !== 'loading';
|
package/dist/index.mjs
CHANGED
|
@@ -11,6 +11,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
|
|
|
11
11
|
...(payload.type !== undefined && { notification_type: payload.type }),
|
|
12
12
|
...(payload.priority !== undefined && { priority: payload.priority }),
|
|
13
13
|
...(payload.tag !== undefined && { tag: payload.tag }),
|
|
14
|
+
...(payload.tags !== undefined && { tags: payload.tags }),
|
|
14
15
|
...(payload.payload !== undefined && { payload: payload.payload }),
|
|
15
16
|
};
|
|
16
17
|
const controller = new AbortController();
|
|
@@ -25,6 +26,7 @@ function sendWebhook(endpoint, token, payload, timeout = DEFAULT_TIMEOUT) {
|
|
|
25
26
|
},
|
|
26
27
|
body: JSON.stringify(body),
|
|
27
28
|
signal: controller.signal,
|
|
29
|
+
keepalive: true,
|
|
28
30
|
})
|
|
29
31
|
.then(async (res) => {
|
|
30
32
|
clearTimeout(timer);
|
|
@@ -71,17 +73,17 @@ function xhrSend(endpoint, token, body, timeout) {
|
|
|
71
73
|
|
|
72
74
|
const SUBMIT_DEBOUNCE_MS = 500;
|
|
73
75
|
const pendingForms = new WeakMap();
|
|
74
|
-
function initFormCapture(onSubmit, root, debug) {
|
|
75
|
-
const forms = root.querySelectorAll('form[data-notify]');
|
|
76
|
+
function initFormCapture(onSubmit, root, debug, onCapture) {
|
|
77
|
+
const forms = root.querySelectorAll('form[data-notix], form[data-notify]');
|
|
76
78
|
forms.forEach((form) => {
|
|
77
79
|
if (form.dataset.notixBound)
|
|
78
80
|
return;
|
|
79
81
|
form.dataset.notixBound = '1';
|
|
80
82
|
form.addEventListener('submit', (e) => {
|
|
83
|
+
e.preventDefault();
|
|
81
84
|
const now = Date.now();
|
|
82
85
|
const last = pendingForms.get(form) ?? 0;
|
|
83
86
|
if (now - last < SUBMIT_DEBOUNCE_MS) {
|
|
84
|
-
e.preventDefault();
|
|
85
87
|
return;
|
|
86
88
|
}
|
|
87
89
|
pendingForms.set(form, now);
|
|
@@ -90,10 +92,19 @@ function initFormCapture(onSubmit, root, debug) {
|
|
|
90
92
|
if (debug) {
|
|
91
93
|
console.log('[Notix] Form captured:', payload.title);
|
|
92
94
|
}
|
|
93
|
-
onSubmit(payload, { title: payload.title, fieldsCount: Object.keys(
|
|
95
|
+
onSubmit(payload, { title: payload.title, fieldsCount: payload.payload ? Object.keys(payload.payload).length : 0 });
|
|
96
|
+
// Per-form callback: data-notix-onsuccess="funcName"
|
|
97
|
+
const callbackName = attr(form, 'onsuccess');
|
|
98
|
+
if (callbackName) {
|
|
99
|
+
const fn = window[callbackName];
|
|
100
|
+
if (typeof fn === 'function') {
|
|
101
|
+
// The actual callback fires after notify() resolves — handled in notix.ts
|
|
102
|
+
form.dataset.notixCallback = callbackName;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (onCapture) ;
|
|
94
106
|
}
|
|
95
107
|
catch (err) {
|
|
96
|
-
// don't block form submission
|
|
97
108
|
if (debug) {
|
|
98
109
|
console.warn('[Notix] Form capture failed:', err);
|
|
99
110
|
}
|
|
@@ -104,64 +115,91 @@ function initFormCapture(onSubmit, root, debug) {
|
|
|
104
115
|
function destroyFormCapture() {
|
|
105
116
|
document.querySelectorAll('form[data-notix-bound]').forEach((form) => {
|
|
106
117
|
delete form.dataset.notixBound;
|
|
107
|
-
// listeners are removed by page navigation; for SPA use Notix.destroy() before unmount
|
|
108
118
|
});
|
|
109
119
|
}
|
|
120
|
+
function attr(form, name) {
|
|
121
|
+
const camel = name.charAt(0).toUpperCase() + name.slice(1);
|
|
122
|
+
return form.dataset[`notix${camel}`] ?? form.dataset[`notify${camel}`];
|
|
123
|
+
}
|
|
124
|
+
function elLabel(el) {
|
|
125
|
+
return el.getAttribute('data-notix-label') ?? el.getAttribute('data-notify-label') ?? el.name;
|
|
126
|
+
}
|
|
110
127
|
function extractPayload(form) {
|
|
111
|
-
const title = form
|
|
112
|
-
const
|
|
113
|
-
const tag = form
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.filter(Boolean);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
...(priority && { priority: priority }),
|
|
128
|
-
};
|
|
128
|
+
const title = attr(form, 'title') ?? document.title;
|
|
129
|
+
const typeSlug = attr(form, 'type');
|
|
130
|
+
const tag = attr(form, 'tag');
|
|
131
|
+
const tagsStr = attr(form, 'tags');
|
|
132
|
+
const priority = attr(form, 'priority');
|
|
133
|
+
const bodyTemplate = attr(form, 'body');
|
|
134
|
+
const fieldsStr = attr(form, 'fields');
|
|
135
|
+
const allowedNames = new Set();
|
|
136
|
+
if (fieldsStr) {
|
|
137
|
+
fieldsStr.split(',').map(s => s.trim()).filter(Boolean).forEach(name => allowedNames.add(name));
|
|
138
|
+
}
|
|
139
|
+
if (bodyTemplate) {
|
|
140
|
+
const matches = bodyTemplate.matchAll(/\{(\w+)\}/g);
|
|
141
|
+
for (const m of matches) {
|
|
142
|
+
allowedNames.add(m[1]);
|
|
143
|
+
}
|
|
129
144
|
}
|
|
145
|
+
let tags;
|
|
146
|
+
if (tag) {
|
|
147
|
+
tags = [tag];
|
|
148
|
+
}
|
|
149
|
+
if (tagsStr) {
|
|
150
|
+
try {
|
|
151
|
+
const parsed = JSON.parse(tagsStr);
|
|
152
|
+
tags = [...(tags || []), ...(Array.isArray(parsed) ? parsed : [parsed])];
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
tags = tags || [];
|
|
156
|
+
tags.push(tagsStr);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const fieldValues = {};
|
|
130
160
|
const elements = form.querySelectorAll('input[name], textarea[name], select[name]');
|
|
131
|
-
const fieldMap = new Map();
|
|
132
161
|
elements.forEach((el) => {
|
|
133
162
|
if (!el.name)
|
|
134
163
|
return;
|
|
164
|
+
if (!allowedNames.has(el.name))
|
|
165
|
+
return;
|
|
135
166
|
if (el.type === 'checkbox' || el.type === 'radio') {
|
|
136
167
|
if (!el.checked)
|
|
137
168
|
return;
|
|
138
169
|
}
|
|
139
|
-
|
|
140
|
-
return;
|
|
141
|
-
const isMarked = el.hasAttribute('data-notify-field');
|
|
142
|
-
if (specifiedFields || isMarked || (!specifiedFields && !template)) {
|
|
143
|
-
const label = el.getAttribute('data-notify-label') ?? el.name;
|
|
144
|
-
fieldMap.set(label, el.value || '');
|
|
145
|
-
}
|
|
170
|
+
fieldValues[el.name] = el.value || '';
|
|
146
171
|
});
|
|
147
172
|
let body;
|
|
148
|
-
if (
|
|
149
|
-
body =
|
|
150
|
-
if (fieldMap.has(key))
|
|
151
|
-
return fieldMap.get(key);
|
|
152
|
-
const el = form.elements.namedItem(key);
|
|
153
|
-
return el?.value || `{${key}}`;
|
|
154
|
-
});
|
|
173
|
+
if (bodyTemplate) {
|
|
174
|
+
body = bodyTemplate.replace(/\{(\w+)\}/g, (_, fieldName) => fieldValues[fieldName] ?? `{${fieldName}}`);
|
|
155
175
|
}
|
|
156
|
-
|
|
157
|
-
|
|
176
|
+
const payloadData = {};
|
|
177
|
+
elements.forEach((el) => {
|
|
178
|
+
if (!el.name)
|
|
179
|
+
return;
|
|
180
|
+
if (!allowedNames.has(el.name))
|
|
181
|
+
return;
|
|
182
|
+
if ((el.type === 'checkbox' || el.type === 'radio') && !el.checked)
|
|
183
|
+
return;
|
|
184
|
+
payloadData[elLabel(el)] = el.value || '';
|
|
185
|
+
});
|
|
186
|
+
let extraPayload = {};
|
|
187
|
+
const payloadJson = attr(form, 'payload');
|
|
188
|
+
if (payloadJson) {
|
|
189
|
+
try {
|
|
190
|
+
extraPayload = JSON.parse(payloadJson);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
// ignore invalid JSON
|
|
194
|
+
}
|
|
158
195
|
}
|
|
159
196
|
return {
|
|
160
197
|
title,
|
|
161
198
|
...(body && { body }),
|
|
162
|
-
...(
|
|
163
|
-
...(
|
|
199
|
+
...(typeSlug && { type: typeSlug }),
|
|
200
|
+
...(tags && tags.length > 0 && { tags }),
|
|
164
201
|
...(priority && { priority: priority }),
|
|
202
|
+
payload: { ...payloadData, ...extraPayload },
|
|
165
203
|
};
|
|
166
204
|
}
|
|
167
205
|
|
|
@@ -296,7 +334,8 @@ class Notix {
|
|
|
296
334
|
this.timeout = config.timeout ?? 10000;
|
|
297
335
|
this.debug = config.debug ?? false;
|
|
298
336
|
this.metrika = createMetrikaTracker(config.metrika);
|
|
299
|
-
|
|
337
|
+
this.onCapture = config.onCapture;
|
|
338
|
+
if (config.autoCapture === true) {
|
|
300
339
|
this.capture();
|
|
301
340
|
}
|
|
302
341
|
if (config.metricEnabled) {
|
|
@@ -328,8 +367,19 @@ class Notix {
|
|
|
328
367
|
if (this.captureActive)
|
|
329
368
|
return;
|
|
330
369
|
initFormCapture((payload, meta) => {
|
|
331
|
-
this.notify(payload).then(() => {
|
|
370
|
+
this.notify(payload).then((response) => {
|
|
332
371
|
this.metrika?.trackFormCaptured(meta.title ?? payload.title, meta.fieldsCount);
|
|
372
|
+
// Per-form callback from data-notix-onsuccess
|
|
373
|
+
const form = (root instanceof Element ? root : document).querySelector('form[data-notix-bound]');
|
|
374
|
+
if (form instanceof HTMLFormElement && form.dataset.notixCallback) {
|
|
375
|
+
const fn = window[form.dataset.notixCallback];
|
|
376
|
+
if (typeof fn === 'function') {
|
|
377
|
+
fn(response);
|
|
378
|
+
}
|
|
379
|
+
delete form.dataset.notixCallback;
|
|
380
|
+
}
|
|
381
|
+
// Global callback from config
|
|
382
|
+
this.onCapture?.(response, payload);
|
|
333
383
|
}).catch(() => { });
|
|
334
384
|
}, root, this.debug);
|
|
335
385
|
this.captureActive = true;
|
|
@@ -398,7 +448,7 @@ if (typeof document !== 'undefined') {
|
|
|
398
448
|
const token = thisScript.dataset.token;
|
|
399
449
|
if (token) {
|
|
400
450
|
const endpoint = thisScript.dataset.endpoint;
|
|
401
|
-
const autoCapture = thisScript.dataset.autoCapture
|
|
451
|
+
const autoCapture = thisScript.dataset.autoCapture === 'true';
|
|
402
452
|
const debug = thisScript.dataset.debug === 'true';
|
|
403
453
|
const timeout = thisScript.dataset.timeout ? parseInt(thisScript.dataset.timeout, 10) : undefined;
|
|
404
454
|
const ready = document.readyState !== 'loading';
|
package/dist/notify.min.js
CHANGED
|
@@ -3,4 +3,4 @@
|
|
|
3
3
|
* @version <%= pkg.version %>
|
|
4
4
|
* @license MIT
|
|
5
5
|
*/
|
|
6
|
-
var Notix=function(t){"use strict";function e(t,e,r
|
|
6
|
+
var Notix=function(t){"use strict";function e(t,e,o,r=1e4){const i={title:o.title,...void 0!==o.body&&{body:o.body},...void 0!==o.type&&{notification_type:o.type},...void 0!==o.priority&&{priority:o.priority},...void 0!==o.tag&&{tag:o.tag},...void 0!==o.tags&&{tags:o.tags},...void 0!==o.payload&&{payload:o.payload}},n=new AbortController,a=setTimeout(()=>n.abort(),r);return"undefined"!=typeof fetch?fetch(t,{method:"POST",headers:{Authorization:`Bearer ${e}`,"Content-Type":"application/json",Accept:"application/json"},body:JSON.stringify(i),signal:n.signal,keepalive:!0}).then(async t=>{clearTimeout(a);const e=await t.json();if(!t.ok)throw new Error(e.error||e.message||`HTTP ${t.status}`);return e}).catch(t=>{throw clearTimeout(a),t}):function(t,e,o,r){return new Promise((i,n)=>{const a=new XMLHttpRequest;a.open("POST",t,!0),a.setRequestHeader("Authorization",`Bearer ${e}`),a.setRequestHeader("Content-Type","application/json"),a.setRequestHeader("Accept","application/json"),a.timeout=r,a.onload=()=>{try{const t=JSON.parse(a.responseText);a.status>=200&&a.status<300?i(t):n(new Error(t.error||t.message||`HTTP ${a.status}`))}catch{n(new Error("Invalid response"))}},a.onerror=()=>n(new Error("Network error")),a.ontimeout=()=>n(new Error("Request timeout")),a.send(JSON.stringify(o))})}(t,e,i,r)}const o=new WeakMap;function r(t,e,r,n){e.querySelectorAll("form[data-notix], form[data-notify]").forEach(e=>{e.dataset.notixBound||(e.dataset.notixBound="1",e.addEventListener("submit",n=>{n.preventDefault();const a=Date.now();if(!(a-(o.get(e)??0)<500)){o.set(e,a);try{const o=function(t){const e=i(t,"title")??document.title,o=i(t,"type"),r=i(t,"tag"),n=i(t,"tags"),a=i(t,"priority"),s=i(t,"body"),c=i(t,"fields"),d=new Set;c&&c.split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>d.add(t));if(s){const t=s.matchAll(/\{(\w+)\}/g);for(const e of t)d.add(e[1])}let l;r&&(l=[r]);if(n)try{const t=JSON.parse(n);l=[...l||[],...Array.isArray(t)?t:[t]]}catch{l=l||[],l.push(n)}const u={},h=t.querySelectorAll("input[name], textarea[name], select[name]");let p;h.forEach(t=>{t.name&&d.has(t.name)&&("checkbox"!==t.type&&"radio"!==t.type||t.checked)&&(u[t.name]=t.value||"")}),s&&(p=s.replace(/\{(\w+)\}/g,(t,e)=>u[e]??`{${e}}`));const f={};h.forEach(t=>{t.name&&d.has(t.name)&&("checkbox"!==t.type&&"radio"!==t.type||t.checked)&&(f[function(t){return t.getAttribute("data-notix-label")??t.getAttribute("data-notify-label")??t.name}(t)]=t.value||"")});let m={};const y=i(t,"payload");if(y)try{m=JSON.parse(y)}catch{}return{title:e,...p&&{body:p},...o&&{type:o},...l&&l.length>0&&{tags:l},...a&&{priority:a},payload:{...f,...m}}}(e);r&&console.log("[Notix] Form captured:",o.title),t(o,{title:o.title,fieldsCount:o.payload?Object.keys(o.payload).length:0});const n=i(e,"onsuccess");if(n){"function"==typeof window[n]&&(e.dataset.notixCallback=n)}}catch(t){r&&console.warn("[Notix] Form capture failed:",t)}}}))})}function i(t,e){const o=e.charAt(0).toUpperCase()+e.slice(1);return t.dataset[`notix${o}`]??t.dataset[`notify${o}`]}function n(t){if(!1===t?.enabled)return null;const e="undefined"!=typeof window&&"function"==typeof window.ym;if(!e&&!t?.enabled)return null;let o=t?.counterId;return!o&&e&&(o=function(){for(const t of Object.keys(window))if(t.startsWith("yaCounter")){const e=parseInt(t.replace("yaCounter",""),10);if(!isNaN(e))return e}return}()),o?new a(o,t?.prefix??"notix"):null}class a{constructor(t,e){this.counterId=t,this.prefix=e}trackSent(t){this.reachGoal(`${this.prefix}_sent`,{id:t.id,message:t.message})}trackError(t){this.reachGoal(`${this.prefix}_error`,{error:t.message})}trackFormCaptured(t,e){this.reachGoal(`${this.prefix}_form_captured`,{title:t,fields:e})}trackGoal(t){this.reachGoal(t,{})}reachGoal(t,e){"function"==typeof window.ym&&window.ym(this.counterId,"reachGoal",t,e)}}class s{constructor(t){this.sendFn=t,this.errors=[],this.timer=null,this.bindGlobal()}bindGlobal(){if("undefined"==typeof window)return;window.addEventListener("error",t=>{this.errors.push({message:t.message||"Unknown error",file:t.filename||"",line:t.lineno||0,col:t.colno||0,stack:t.error?.stack||"",ts:(new Date).toISOString()}),this.errors.length>=10&&this.flush(),this.startTimer()}),window.addEventListener("unhandledrejection",t=>{this.errors.push({message:t.reason?.message||String(t.reason),file:"",line:0,col:0,stack:t.reason?.stack||"",ts:(new Date).toISOString()}),this.errors.length>=10&&this.flush(),this.startTimer()}),window.addEventListener("beforeunload",()=>this.flush()),document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&this.flush()})}startTimer(){this.timer||(this.timer=setTimeout(()=>this.flush(),6e4))}flush(){0!==this.errors.length&&(this.sendFn([...this.errors]),this.errors=[],this.timer&&(clearTimeout(this.timer),this.timer=null))}}class c{constructor(t){if(this.captureActive=!1,!t.token||!t.token.startsWith("ntx_"))throw new Error('Notix: token must start with "ntx_"');this.token=t.token,this.endpoint=t.endpoint??"https://notix-hub.ru/api/v1/webhook",this.timeout=t.timeout??1e4,this.debug=t.debug??!1,this.metrika=n(t.metrika),this.onCapture=t.onCapture,!0===t.autoCapture&&this.capture(),t.metricEnabled&&this.sendPageview(),t.errorTrackingEnabled&&this.enableErrorTracking()}async notify(t){if(!t.title)throw new Error("Notix: title is required");this.log("Sending notification:",t.title);try{const o=await e(this.endpoint,this.token,t,this.timeout);return this.log("Notification sent:",o.id),this.metrika?.trackSent(o),o}catch(t){const e=t instanceof Error?t:new Error(String(t));throw this.log("Error sending notification:",e.message),this.metrika?.trackError(e),e}}capture(t=document){this.captureActive||(r((e,o)=>{this.notify(e).then(r=>{this.metrika?.trackFormCaptured(o.title??e.title,o.fieldsCount);const i=(t instanceof Element?t:document).querySelector("form[data-notix-bound]");if(i instanceof HTMLFormElement&&i.dataset.notixCallback){const t=window[i.dataset.notixCallback];"function"==typeof t&&t(r),delete i.dataset.notixCallback}this.onCapture?.(r,e)}).catch(()=>{})},t,this.debug),this.captureActive=!0,this.log("Form capture activated"))}destroy(){document.querySelectorAll("form[data-notix-bound]").forEach(t=>{delete t.dataset.notixBound}),this.captureActive=!1,this.log("Notix destroyed")}sendPageview(t,e){const o={title:"Посетитель на сайте",type:"metric",payload:{event_type:"pageview",visitor_id:l(),page:t||location.pathname,referrer:e||document.referrer||void 0,value:1}};this.notify(o).catch(()=>{}),this.log("Pageview sent:",t||location.pathname)}enableErrorTracking(){"undefined"!=typeof window&&(new s(t=>{this.notify({title:`Ошибки на странице (${t.length})`,type:"error_batch",payload:{errors:t}}).catch(()=>{})}),this.log("Error tracking activated"))}static getVisitorId(){return l()}log(...t){this.debug&&console.log("[Notix]",...t)}}const d="notix_vid";function l(){try{let t=localStorage.getItem(d);return t||(t=crypto.randomUUID(),localStorage.setItem(d,t)),t}catch{return"unknown"}}if("undefined"!=typeof document){const t=document.currentScript;if(t){const e=t.dataset.token;if(e){const o=t.dataset.endpoint,r="true"===t.dataset.autoCapture,i="true"===t.dataset.debug,n=t.dataset.timeout?parseInt(t.dataset.timeout,10):void 0,a=()=>{const a="true"===t.dataset.metric,s="true"===t.dataset.errors,d=new c({token:e,...o&&{endpoint:o},autoCapture:r,debug:i,metricEnabled:a,errorTrackingEnabled:s,...n&&{timeout:n}});window.notix=d};"loading"!==document.readyState?a():document.addEventListener("DOMContentLoaded",a)}else"true"===t.dataset.debug&&console.warn("[Notix] data-token attribute is required on script tag")}}return t.Notix=c,t}({});
|
package/dist/notix.d.ts
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
export type NotificationPriority = 'low' | 'normal' | 'high' | 'urgent';
|
|
2
|
+
export interface TagObject {
|
|
3
|
+
name: string;
|
|
4
|
+
color?: string;
|
|
5
|
+
}
|
|
2
6
|
export interface NotifyPayload {
|
|
3
7
|
title: string;
|
|
4
8
|
body?: string;
|
|
5
9
|
type?: string;
|
|
6
10
|
tag?: string;
|
|
11
|
+
tags?: Array<string | TagObject>;
|
|
7
12
|
priority?: NotificationPriority;
|
|
8
13
|
payload?: Record<string, unknown>;
|
|
9
14
|
}
|
|
10
|
-
export interface NotifyRequestBody
|
|
15
|
+
export interface NotifyRequestBody {
|
|
16
|
+
title: string;
|
|
17
|
+
body?: string;
|
|
11
18
|
notification_type?: string;
|
|
12
19
|
tag?: string;
|
|
20
|
+
tags?: Array<string | TagObject>;
|
|
21
|
+
priority?: string;
|
|
22
|
+
payload?: Record<string, unknown>;
|
|
13
23
|
}
|
|
14
24
|
export interface NotixResponse {
|
|
15
25
|
message: string;
|
|
@@ -29,4 +39,5 @@ export interface NotixConfig {
|
|
|
29
39
|
metrika?: MetrikaConfig;
|
|
30
40
|
timeout?: number;
|
|
31
41
|
debug?: boolean;
|
|
42
|
+
onCapture?: (response: NotixResponse, payload: NotifyPayload) => void;
|
|
32
43
|
}
|