@silverassist/leadcapture-form 0.1.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/CHANGELOG.md +19 -0
- package/LICENSE +131 -0
- package/README.md +94 -0
- package/dist/index.d.mts +74 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +194 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +192 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +114 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - Unreleased
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial extraction of `LeadCaptureForm` from the fleet's `ScriptManager`
|
|
10
|
+
implementations, ported onto `@silverassist/next-script-loader`.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- The per-variant `ScriptManager` (independent state per variant) was
|
|
15
|
+
replaced by `ScriptLoader`'s single-active-variant model. Ownership
|
|
16
|
+
arbitration and ref-counted load/unload now come from `ScriptLoader`
|
|
17
|
+
directly; only the generation-based stale-unmount guard (needed because
|
|
18
|
+
`reload()` doesn't change the reference count) stays as package-local
|
|
19
|
+
logic on top of it. See the README's "Known behavioral difference" note.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Polyform Noncommercial License 1.0.0
|
|
2
|
+
|
|
3
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
4
|
+
|
|
5
|
+
## Acceptance
|
|
6
|
+
|
|
7
|
+
In order to get any license under these terms, you must agree
|
|
8
|
+
to them as both strict obligations and conditions to all
|
|
9
|
+
your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the
|
|
14
|
+
software to do everything you might do with the software
|
|
15
|
+
that would otherwise infringe the licensor's copyright
|
|
16
|
+
in it for any permitted purpose. However, you may
|
|
17
|
+
only distribute the software according to [Distribution
|
|
18
|
+
License](#distribution-license) and make changes or new works
|
|
19
|
+
based on the software according to [Changes and New Works
|
|
20
|
+
License](#changes-and-new-works-license).
|
|
21
|
+
|
|
22
|
+
## Distribution License
|
|
23
|
+
|
|
24
|
+
The licensor grants you an additional copyright license
|
|
25
|
+
to distribute copies of the software. Your license
|
|
26
|
+
to distribute covers distributing the software with
|
|
27
|
+
changes and new works permitted by [Changes and New Works
|
|
28
|
+
License](#changes-and-new-works-license).
|
|
29
|
+
|
|
30
|
+
## Notices
|
|
31
|
+
|
|
32
|
+
You must ensure that anyone who gets a copy of any part of
|
|
33
|
+
the software from you also gets a copy of these terms or the
|
|
34
|
+
URL for them above, as well as copies of any plain-text lines
|
|
35
|
+
beginning with `Required Notice:` that the licensor provided
|
|
36
|
+
with the software. For example:
|
|
37
|
+
|
|
38
|
+
> Required Notice: Copyright Silver Assist Team
|
|
39
|
+
|
|
40
|
+
## Changes and New Works License
|
|
41
|
+
|
|
42
|
+
The licensor grants you an additional copyright license to
|
|
43
|
+
make changes and new works based on the software for any
|
|
44
|
+
permitted purpose.
|
|
45
|
+
|
|
46
|
+
## Patent License
|
|
47
|
+
|
|
48
|
+
The licensor grants you a patent license for the software that
|
|
49
|
+
covers patent claims the licensor can license, or becomes able
|
|
50
|
+
to license, that you would infringe by using the software.
|
|
51
|
+
|
|
52
|
+
## Noncommercial Purposes
|
|
53
|
+
|
|
54
|
+
Any noncommercial purpose is a permitted purpose.
|
|
55
|
+
|
|
56
|
+
## Personal Uses
|
|
57
|
+
|
|
58
|
+
Personal use for research, experiment, and testing for
|
|
59
|
+
the benefit of public knowledge, personal study, private
|
|
60
|
+
entertainment, hobby projects, amateur pursuits, or religious
|
|
61
|
+
observance, without any anticipated commercial application,
|
|
62
|
+
is use for a permitted purpose.
|
|
63
|
+
|
|
64
|
+
## Noncommercial Organizations
|
|
65
|
+
|
|
66
|
+
Use by any charitable organization, educational institution,
|
|
67
|
+
public research organization, public safety or health
|
|
68
|
+
organization, environmental protection organization,
|
|
69
|
+
or government institution is use for a permitted purpose
|
|
70
|
+
regardless of the source of funding or obligations resulting
|
|
71
|
+
from the funding.
|
|
72
|
+
|
|
73
|
+
## Fair Use
|
|
74
|
+
|
|
75
|
+
You may have "fair use" rights for the software under the
|
|
76
|
+
law. These terms do not limit them.
|
|
77
|
+
|
|
78
|
+
## No Other Rights
|
|
79
|
+
|
|
80
|
+
These terms do not allow you to sublicense or transfer any of
|
|
81
|
+
your licenses to anyone else, or prevent the licensor from
|
|
82
|
+
granting licenses to anyone else. These terms do not imply
|
|
83
|
+
any other licenses.
|
|
84
|
+
|
|
85
|
+
## Patent Defense
|
|
86
|
+
|
|
87
|
+
If you make any written claim that the software infringes or
|
|
88
|
+
contributes to infringement of any patent, your patent license
|
|
89
|
+
for the software granted under these terms ends immediately. If
|
|
90
|
+
your company makes such a claim, your patent license ends
|
|
91
|
+
immediately for work on behalf of your company.
|
|
92
|
+
|
|
93
|
+
## Violations
|
|
94
|
+
|
|
95
|
+
The first time you are notified in writing that you have
|
|
96
|
+
violated any of these terms, or done anything with the software
|
|
97
|
+
not covered by your licenses, your licenses can nonetheless
|
|
98
|
+
continue if you come into full compliance with these terms,
|
|
99
|
+
and take practical steps to correct past violations, within
|
|
100
|
+
32 days of receiving notice. Otherwise, all your licenses
|
|
101
|
+
end immediately.
|
|
102
|
+
|
|
103
|
+
## No Liability
|
|
104
|
+
|
|
105
|
+
***As far as the law allows, the software comes as is, without
|
|
106
|
+
any warranty or condition, and the licensor will not be liable
|
|
107
|
+
to you for any damages arising out of these terms or the use
|
|
108
|
+
or nature of the software, under any kind of legal claim.***
|
|
109
|
+
|
|
110
|
+
## Definitions
|
|
111
|
+
|
|
112
|
+
The **licensor** is the individual or entity offering these
|
|
113
|
+
terms, and the **software** is the software the licensor makes
|
|
114
|
+
available under these terms.
|
|
115
|
+
|
|
116
|
+
**You** refers to the individual or entity agreeing to these
|
|
117
|
+
terms.
|
|
118
|
+
|
|
119
|
+
**Your company** is any legal entity, sole proprietorship,
|
|
120
|
+
or other kind of organization that you work for, plus all
|
|
121
|
+
organizations that have control over, are under the control of,
|
|
122
|
+
or are under common control with that organization. **Control**
|
|
123
|
+
means ownership of substantially all the assets of an entity,
|
|
124
|
+
or the power to direct its management and policies by vote,
|
|
125
|
+
contract, or otherwise. Control can be direct or indirect.
|
|
126
|
+
|
|
127
|
+
**Your licenses** are all the licenses granted to you for the
|
|
128
|
+
software under these terms.
|
|
129
|
+
|
|
130
|
+
**Use** means anything you do with the software requiring one
|
|
131
|
+
of your licenses.
|
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @silverassist/leadcapture-form
|
|
2
|
+
|
|
3
|
+
LeadCapture IO form integration for Next.js apps. Variant switching,
|
|
4
|
+
ownership arbitration between competing components (e.g. a modal and an
|
|
5
|
+
on-page form), and ref-counted script lifecycle — built on
|
|
6
|
+
`@silverassist/next-script-loader`.
|
|
7
|
+
|
|
8
|
+
## Status
|
|
9
|
+
|
|
10
|
+
Extracted from the fleet's `ScriptManager` implementations — the pattern
|
|
11
|
+
`@silverassist/next-script-loader` itself was generalized from (see that
|
|
12
|
+
package's own doc comment). Not yet published. Part of the fleet-wide
|
|
13
|
+
third-party-integration package effort described in
|
|
14
|
+
`nextjs-boilerplate/docs/NEXTJS_CORE_PACKAGE_PLAN.md`.
|
|
15
|
+
|
|
16
|
+
**Known behavioral difference from the site implementations this was ported
|
|
17
|
+
from:** the shared `leadCaptureLoader` tracks one active variant at a time —
|
|
18
|
+
loading a different variant tears down the previous one, the same way
|
|
19
|
+
`ScriptLoader.load()` always has. The original per-site `ScriptManager`
|
|
20
|
+
tracked state per variant independently, so two different variants could in
|
|
21
|
+
principle stay loaded simultaneously (e.g. a modal on `"desktop"` and an
|
|
22
|
+
on-page form on `"mobile"` on the same page at once). If your site actually
|
|
23
|
+
needs that, instantiate a second `ScriptLoader` yourself rather than sharing
|
|
24
|
+
`leadCaptureLoader` — see [Advanced: two independent
|
|
25
|
+
loaders](#advanced-two-independent-loaders).
|
|
26
|
+
|
|
27
|
+
LeadCapture IO serves one shared script for every variant — it reads which
|
|
28
|
+
form to render from a global `window.form_token` this component sets from
|
|
29
|
+
`formTokens[formVariant]` right before loading, rather than varying the
|
|
30
|
+
script URL per variant.
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @silverassist/leadcapture-form
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
```tsx
|
|
41
|
+
import LeadCaptureForm from "@silverassist/leadcapture-form";
|
|
42
|
+
|
|
43
|
+
const FORM_TOKENS = { desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" };
|
|
44
|
+
|
|
45
|
+
// Modal usage
|
|
46
|
+
<LeadCaptureForm
|
|
47
|
+
formVariant="desktop"
|
|
48
|
+
formTokens={FORM_TOKENS}
|
|
49
|
+
usageContext="modal"
|
|
50
|
+
isModalOpen={isOpen}
|
|
51
|
+
/>;
|
|
52
|
+
|
|
53
|
+
// On-page usage
|
|
54
|
+
<LeadCaptureForm
|
|
55
|
+
formVariant="mobile"
|
|
56
|
+
formTokens={FORM_TOKENS}
|
|
57
|
+
usageContext="onPage"
|
|
58
|
+
isModalOpen={true}
|
|
59
|
+
/>;
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
A single form can render in multiple DOM locations via the
|
|
63
|
+
`.leadforms-embd-form` class inside the component's container — LeadCapture
|
|
64
|
+
IO's script populates every matching div once it loads, it doesn't re-run
|
|
65
|
+
per div.
|
|
66
|
+
|
|
67
|
+
### Props
|
|
68
|
+
|
|
69
|
+
| Prop | Type | Default | Description |
|
|
70
|
+
| --------------- | ------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------ |
|
|
71
|
+
| `formVariant` | `string` | — | Required. Must match a key in `formTokens` (e.g. `"desktop"`, `"itt"`). |
|
|
72
|
+
| `formTokens` | `Record<string, string>` | — | Required. Maps variant names to LeadCapture IO form tokens. |
|
|
73
|
+
| `scriptUrl` | `string` | LeadCapture IO's default CDN URL | Override the embed script URL. |
|
|
74
|
+
| `usageContext` | `"modal" \| "onPage"` | — | Required. Controls when the script loads. |
|
|
75
|
+
| `isModalOpen` | `boolean` | — | Required. Only meaningful for `usageContext="modal"`. |
|
|
76
|
+
| `className` | `string` | `""` | CSS classes on the container. |
|
|
77
|
+
| `embedTargetId` | `string` | — | Id for the inner `.leadforms-embd-form` div (e.g. matching a WordPress `embed_target_id`). |
|
|
78
|
+
|
|
79
|
+
## Advanced: two independent loaders
|
|
80
|
+
|
|
81
|
+
If a page genuinely needs two different variants active at once, don't rely
|
|
82
|
+
on the module-level `leadCaptureLoader` singleton for both — the underlying
|
|
83
|
+
`@silverassist/next-script-loader` `ScriptLoader` only tracks one variant per
|
|
84
|
+
instance. Fork this component (or file an issue) rather than sharing the
|
|
85
|
+
export across two forms that need independent variants simultaneously; this
|
|
86
|
+
is a real gap, not a documented-and-solved case, in v0.1.0.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
[PolyForm Noncommercial 1.0.0](./LICENSE)
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
Made with ❤️ by Silver Assist
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ScriptLoader } from "@silverassist/next-script-loader";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
type UsageContext = "modal" | "onPage";
|
|
5
|
+
/**
|
|
6
|
+
* Module-level singleton: every `LeadCaptureForm` instance on the page
|
|
7
|
+
* shares one loader. `ScriptLoader` tracks a single active variant at a
|
|
8
|
+
* time — switching variants tears down the previous one — which matches
|
|
9
|
+
* how this form is actually used in the fleet (one device/territory
|
|
10
|
+
* variant active per page). A page that genuinely needs two different
|
|
11
|
+
* variants loaded simultaneously (e.g. a modal on "desktop" and an on-page
|
|
12
|
+
* form on "mobile" at once) isn't supported by this shared instance; see
|
|
13
|
+
* the README for the workaround.
|
|
14
|
+
*/
|
|
15
|
+
declare const leadCaptureLoader: ScriptLoader;
|
|
16
|
+
interface LeadCaptureFormProps {
|
|
17
|
+
/**
|
|
18
|
+
* Form variant to render (e.g. "desktop", "mobile", "itt", "oot"). Must
|
|
19
|
+
* match a key configured in `formTokens`.
|
|
20
|
+
*/
|
|
21
|
+
formVariant: string;
|
|
22
|
+
/** Map of variant names to LeadCapture IO form tokens. */
|
|
23
|
+
formTokens: Record<string, string>;
|
|
24
|
+
/** Script URL override, if not using LeadCapture IO's default CDN. */
|
|
25
|
+
scriptUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Usage context — determines loading behavior.
|
|
28
|
+
* - `modal`: loads when the modal opens
|
|
29
|
+
* - `onPage`: loads when in viewport, after minimal interaction
|
|
30
|
+
*/
|
|
31
|
+
usageContext: UsageContext;
|
|
32
|
+
/** Controls whether the modal is open (only relevant for `usageContext="modal"`). */
|
|
33
|
+
isModalOpen: boolean;
|
|
34
|
+
/** Optional additional CSS classes. */
|
|
35
|
+
className?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional embed target id for the inner `.leadforms-embd-form` div. Must
|
|
38
|
+
* match the WordPress `embed_target_id` when the site sources form
|
|
39
|
+
* placement from WordPress.
|
|
40
|
+
*/
|
|
41
|
+
embedTargetId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* LeadCaptureForm — renders a LeadCapture IO form with support for
|
|
45
|
+
* configurable variants, built on `@silverassist/next-script-loader`'s
|
|
46
|
+
* singleton, reference-counted, ownership-arbitrated script lifecycle.
|
|
47
|
+
*
|
|
48
|
+
* A single form can render in multiple DOM locations via the
|
|
49
|
+
* `.leadforms-embd-form` class — LeadCapture IO's script populates every
|
|
50
|
+
* matching div once it loads, it doesn't re-run per div.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // Modal usage
|
|
55
|
+
* <LeadCaptureForm
|
|
56
|
+
* formVariant="desktop"
|
|
57
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
58
|
+
* usageContext="modal"
|
|
59
|
+
* isModalOpen={isOpen}
|
|
60
|
+
* />
|
|
61
|
+
*
|
|
62
|
+
* // On-page usage
|
|
63
|
+
* <LeadCaptureForm
|
|
64
|
+
* formVariant="mobile"
|
|
65
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
66
|
+
* usageContext="onPage"
|
|
67
|
+
* isModalOpen={true}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isModalOpen, className, embedTargetId }: LeadCaptureFormProps): import("react").JSX.Element;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { LeadCaptureFormProps, UsageContext, LeadCaptureForm as default, leadCaptureLoader };
|
|
74
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { ScriptLoader } from "@silverassist/next-script-loader";
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
type UsageContext = "modal" | "onPage";
|
|
5
|
+
/**
|
|
6
|
+
* Module-level singleton: every `LeadCaptureForm` instance on the page
|
|
7
|
+
* shares one loader. `ScriptLoader` tracks a single active variant at a
|
|
8
|
+
* time — switching variants tears down the previous one — which matches
|
|
9
|
+
* how this form is actually used in the fleet (one device/territory
|
|
10
|
+
* variant active per page). A page that genuinely needs two different
|
|
11
|
+
* variants loaded simultaneously (e.g. a modal on "desktop" and an on-page
|
|
12
|
+
* form on "mobile" at once) isn't supported by this shared instance; see
|
|
13
|
+
* the README for the workaround.
|
|
14
|
+
*/
|
|
15
|
+
declare const leadCaptureLoader: ScriptLoader;
|
|
16
|
+
interface LeadCaptureFormProps {
|
|
17
|
+
/**
|
|
18
|
+
* Form variant to render (e.g. "desktop", "mobile", "itt", "oot"). Must
|
|
19
|
+
* match a key configured in `formTokens`.
|
|
20
|
+
*/
|
|
21
|
+
formVariant: string;
|
|
22
|
+
/** Map of variant names to LeadCapture IO form tokens. */
|
|
23
|
+
formTokens: Record<string, string>;
|
|
24
|
+
/** Script URL override, if not using LeadCapture IO's default CDN. */
|
|
25
|
+
scriptUrl?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Usage context — determines loading behavior.
|
|
28
|
+
* - `modal`: loads when the modal opens
|
|
29
|
+
* - `onPage`: loads when in viewport, after minimal interaction
|
|
30
|
+
*/
|
|
31
|
+
usageContext: UsageContext;
|
|
32
|
+
/** Controls whether the modal is open (only relevant for `usageContext="modal"`). */
|
|
33
|
+
isModalOpen: boolean;
|
|
34
|
+
/** Optional additional CSS classes. */
|
|
35
|
+
className?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Optional embed target id for the inner `.leadforms-embd-form` div. Must
|
|
38
|
+
* match the WordPress `embed_target_id` when the site sources form
|
|
39
|
+
* placement from WordPress.
|
|
40
|
+
*/
|
|
41
|
+
embedTargetId?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* LeadCaptureForm — renders a LeadCapture IO form with support for
|
|
45
|
+
* configurable variants, built on `@silverassist/next-script-loader`'s
|
|
46
|
+
* singleton, reference-counted, ownership-arbitrated script lifecycle.
|
|
47
|
+
*
|
|
48
|
+
* A single form can render in multiple DOM locations via the
|
|
49
|
+
* `.leadforms-embd-form` class — LeadCapture IO's script populates every
|
|
50
|
+
* matching div once it loads, it doesn't re-run per div.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // Modal usage
|
|
55
|
+
* <LeadCaptureForm
|
|
56
|
+
* formVariant="desktop"
|
|
57
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
58
|
+
* usageContext="modal"
|
|
59
|
+
* isModalOpen={isOpen}
|
|
60
|
+
* />
|
|
61
|
+
*
|
|
62
|
+
* // On-page usage
|
|
63
|
+
* <LeadCaptureForm
|
|
64
|
+
* formVariant="mobile"
|
|
65
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
66
|
+
* usageContext="onPage"
|
|
67
|
+
* isModalOpen={true}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isModalOpen, className, embedTargetId }: LeadCaptureFormProps): import("react").JSX.Element;
|
|
72
|
+
//#endregion
|
|
73
|
+
export { LeadCaptureFormProps, UsageContext, LeadCaptureForm as default, leadCaptureLoader };
|
|
74
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
5
|
+
let _silverassist_next_script_loader = require("@silverassist/next-script-loader");
|
|
6
|
+
let react = require("react");
|
|
7
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
|
|
9
|
+
//#region src/index.tsx
|
|
10
|
+
/**
|
|
11
|
+
* @packageDocumentation
|
|
12
|
+
* LeadCapture IO form integration for Next.js — a variant-switching,
|
|
13
|
+
* ownership-arbitrated `LeadCaptureForm` component built on
|
|
14
|
+
* `@silverassist/next-script-loader`.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Module-level singleton: every `LeadCaptureForm` instance on the page
|
|
18
|
+
* shares one loader. `ScriptLoader` tracks a single active variant at a
|
|
19
|
+
* time — switching variants tears down the previous one — which matches
|
|
20
|
+
* how this form is actually used in the fleet (one device/territory
|
|
21
|
+
* variant active per page). A page that genuinely needs two different
|
|
22
|
+
* variants loaded simultaneously (e.g. a modal on "desktop" and an on-page
|
|
23
|
+
* form on "mobile" at once) isn't supported by this shared instance; see
|
|
24
|
+
* the README for the workaround.
|
|
25
|
+
*/
|
|
26
|
+
const leadCaptureLoader = new _silverassist_next_script_loader.ScriptLoader();
|
|
27
|
+
/**
|
|
28
|
+
* Generation counter per variant, incremented on every {@link ScriptLoader.load}/
|
|
29
|
+
* {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the
|
|
30
|
+
* generation at mount time and skips its `unload()` call if the generation has
|
|
31
|
+
* since advanced — i.e. a new mount already reloaded the script before the old
|
|
32
|
+
* mount's delayed cleanup ran (a page-navigation remount, not a real teardown).
|
|
33
|
+
* `ScriptLoader`'s own ref-counting handles the common case; this guards the
|
|
34
|
+
* one case it doesn't: `reload()` doesn't change the reference count, so a
|
|
35
|
+
* stale `unload()` after a `reload()` could drop the count to zero and tear
|
|
36
|
+
* down a script a fresh mount is now depending on.
|
|
37
|
+
*/
|
|
38
|
+
const generationByVariant = /* @__PURE__ */ new Map();
|
|
39
|
+
function bumpGeneration(variant) {
|
|
40
|
+
const next = (generationByVariant.get(variant) ?? 0) + 1;
|
|
41
|
+
generationByVariant.set(variant, next);
|
|
42
|
+
return next;
|
|
43
|
+
}
|
|
44
|
+
function currentGeneration(variant) {
|
|
45
|
+
return generationByVariant.get(variant) ?? 0;
|
|
46
|
+
}
|
|
47
|
+
const DEFAULT_LEADCAPTURE_SCRIPT_URL = "https://api.useleadbot.com/lead-bots/get-pixel-script.js";
|
|
48
|
+
/**
|
|
49
|
+
* LeadCaptureForm — renders a LeadCapture IO form with support for
|
|
50
|
+
* configurable variants, built on `@silverassist/next-script-loader`'s
|
|
51
|
+
* singleton, reference-counted, ownership-arbitrated script lifecycle.
|
|
52
|
+
*
|
|
53
|
+
* A single form can render in multiple DOM locations via the
|
|
54
|
+
* `.leadforms-embd-form` class — LeadCapture IO's script populates every
|
|
55
|
+
* matching div once it loads, it doesn't re-run per div.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```tsx
|
|
59
|
+
* // Modal usage
|
|
60
|
+
* <LeadCaptureForm
|
|
61
|
+
* formVariant="desktop"
|
|
62
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
63
|
+
* usageContext="modal"
|
|
64
|
+
* isModalOpen={isOpen}
|
|
65
|
+
* />
|
|
66
|
+
*
|
|
67
|
+
* // On-page usage
|
|
68
|
+
* <LeadCaptureForm
|
|
69
|
+
* formVariant="mobile"
|
|
70
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
71
|
+
* usageContext="onPage"
|
|
72
|
+
* isModalOpen={true}
|
|
73
|
+
* />
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isModalOpen, className = "", embedTargetId }) {
|
|
77
|
+
const [isInViewport, setIsInViewport] = (0, react.useState)(false);
|
|
78
|
+
const formRef = (0, react.useRef)(null);
|
|
79
|
+
const mountGenRef = (0, react.useRef)(0);
|
|
80
|
+
const isMountedRef = (0, react.useRef)(true);
|
|
81
|
+
const containerId = `leadcapture-container-${formVariant}-${usageContext}`;
|
|
82
|
+
(0, react.useEffect)(() => {
|
|
83
|
+
isMountedRef.current = true;
|
|
84
|
+
return () => {
|
|
85
|
+
isMountedRef.current = false;
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
(0, react.useEffect)(() => {
|
|
89
|
+
leadCaptureLoader.configure({ urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL } });
|
|
90
|
+
}, [formVariant, scriptUrl]);
|
|
91
|
+
/**
|
|
92
|
+
* Intersection Observer for onPage forms — loads when near viewport.
|
|
93
|
+
*/
|
|
94
|
+
(0, react.useEffect)(() => {
|
|
95
|
+
if (usageContext !== "onPage" || !formRef.current) return;
|
|
96
|
+
const observer = new IntersectionObserver((entries) => {
|
|
97
|
+
entries.forEach((entry) => {
|
|
98
|
+
if (entry.isIntersecting) {
|
|
99
|
+
setIsInViewport(true);
|
|
100
|
+
observer.disconnect();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}, {
|
|
104
|
+
rootMargin: "100px",
|
|
105
|
+
threshold: .1
|
|
106
|
+
});
|
|
107
|
+
observer.observe(formRef.current);
|
|
108
|
+
return () => observer.disconnect();
|
|
109
|
+
}, [usageContext]);
|
|
110
|
+
/**
|
|
111
|
+
* Script loading with minimal interaction pattern.
|
|
112
|
+
* - Modal: loads immediately when the modal opens.
|
|
113
|
+
* - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once
|
|
114
|
+
* near viewport.
|
|
115
|
+
*/
|
|
116
|
+
(0, react.useEffect)(() => {
|
|
117
|
+
if (!(usageContext === "modal" ? isModalOpen === true : isInViewport)) return;
|
|
118
|
+
const load = () => {
|
|
119
|
+
window.form_token = formTokens[formVariant];
|
|
120
|
+
bumpGeneration(formVariant);
|
|
121
|
+
leadCaptureLoader.load(formVariant).then(() => {
|
|
122
|
+
if (!isMountedRef.current) return;
|
|
123
|
+
leadCaptureLoader.forceSetOwner(containerId);
|
|
124
|
+
}).catch(() => {});
|
|
125
|
+
};
|
|
126
|
+
if (usageContext === "modal") {
|
|
127
|
+
load();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const events = [
|
|
131
|
+
"focus",
|
|
132
|
+
"mousemove",
|
|
133
|
+
"scroll",
|
|
134
|
+
"touchstart"
|
|
135
|
+
];
|
|
136
|
+
const loadOnce = () => {
|
|
137
|
+
load();
|
|
138
|
+
events.forEach((event) => document.removeEventListener(event, loadOnce));
|
|
139
|
+
};
|
|
140
|
+
events.forEach((event) => {
|
|
141
|
+
document.addEventListener(event, loadOnce, { once: true });
|
|
142
|
+
});
|
|
143
|
+
return () => {
|
|
144
|
+
events.forEach((event) => document.removeEventListener(event, loadOnce));
|
|
145
|
+
};
|
|
146
|
+
}, [
|
|
147
|
+
isModalOpen,
|
|
148
|
+
isInViewport,
|
|
149
|
+
formVariant,
|
|
150
|
+
usageContext,
|
|
151
|
+
containerId,
|
|
152
|
+
formTokens
|
|
153
|
+
]);
|
|
154
|
+
/**
|
|
155
|
+
* Capture the current generation at mount time — passed to the delayed
|
|
156
|
+
* unload on cleanup so a stale unmount (superseded by a fresh mount that
|
|
157
|
+
* already reloaded) is a no-op.
|
|
158
|
+
*/
|
|
159
|
+
(0, react.useEffect)(() => {
|
|
160
|
+
mountGenRef.current = currentGeneration(formVariant);
|
|
161
|
+
}, [formVariant]);
|
|
162
|
+
/**
|
|
163
|
+
* Cleanup on unmount only.
|
|
164
|
+
* Modal: only releases ownership, doesn't unload the script.
|
|
165
|
+
* OnPage: releases ownership and unloads after a short delay, skipped if
|
|
166
|
+
* a newer mount has already reloaded the script in the meantime.
|
|
167
|
+
*/
|
|
168
|
+
(0, react.useEffect)(() => {
|
|
169
|
+
return () => {
|
|
170
|
+
leadCaptureLoader.releaseOwnership(containerId);
|
|
171
|
+
if (usageContext === "onPage") {
|
|
172
|
+
const gen = mountGenRef.current;
|
|
173
|
+
setTimeout(() => {
|
|
174
|
+
if (gen < currentGeneration(formVariant)) return;
|
|
175
|
+
leadCaptureLoader.unload();
|
|
176
|
+
}, 100);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}, []);
|
|
180
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
181
|
+
ref: formRef,
|
|
182
|
+
id: containerId,
|
|
183
|
+
className,
|
|
184
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
185
|
+
className: "leadforms-embd-form",
|
|
186
|
+
...embedTargetId ? { id: embedTargetId } : {}
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
//#endregion
|
|
192
|
+
exports.default = LeadCaptureForm;
|
|
193
|
+
exports.leadCaptureLoader = leadCaptureLoader;
|
|
194
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["ScriptLoader","useState","useRef"],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\n/**\n * Generation counter per variant, incremented on every {@link ScriptLoader.load}/\n * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the\n * generation at mount time and skips its `unload()` call if the generation has\n * since advanced — i.e. a new mount already reloaded the script before the old\n * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).\n * `ScriptLoader`'s own ref-counting handles the common case; this guards the\n * one case it doesn't: `reload()` doesn't change the reference count, so a\n * stale `unload()` after a `reload()` could drop the count to zero and tear\n * down a script a fresh mount is now depending on.\n */\nconst generationByVariant = new Map<string, number>();\n\nfunction bumpGeneration(variant: string): number {\n const next = (generationByVariant.get(variant) ?? 0) + 1;\n generationByVariant.set(variant, next);\n return next;\n}\n\nfunction currentGeneration(variant: string): number {\n return generationByVariant.get(variant) ?? 0;\n}\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n bumpGeneration(formVariant);\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = currentGeneration(formVariant);\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n if (gen < currentGeneration(formVariant)) return;\n leadCaptureLoader.unload();\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import { ScriptLoader } from "@silverassist/next-script-loader";
|
|
5
|
+
import { useEffect, useRef, useState } from "react";
|
|
6
|
+
import { jsx } from "react/jsx-runtime";
|
|
7
|
+
|
|
8
|
+
//#region src/index.tsx
|
|
9
|
+
/**
|
|
10
|
+
* @packageDocumentation
|
|
11
|
+
* LeadCapture IO form integration for Next.js — a variant-switching,
|
|
12
|
+
* ownership-arbitrated `LeadCaptureForm` component built on
|
|
13
|
+
* `@silverassist/next-script-loader`.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Module-level singleton: every `LeadCaptureForm` instance on the page
|
|
17
|
+
* shares one loader. `ScriptLoader` tracks a single active variant at a
|
|
18
|
+
* time — switching variants tears down the previous one — which matches
|
|
19
|
+
* how this form is actually used in the fleet (one device/territory
|
|
20
|
+
* variant active per page). A page that genuinely needs two different
|
|
21
|
+
* variants loaded simultaneously (e.g. a modal on "desktop" and an on-page
|
|
22
|
+
* form on "mobile" at once) isn't supported by this shared instance; see
|
|
23
|
+
* the README for the workaround.
|
|
24
|
+
*/
|
|
25
|
+
const leadCaptureLoader = new ScriptLoader();
|
|
26
|
+
/**
|
|
27
|
+
* Generation counter per variant, incremented on every {@link ScriptLoader.load}/
|
|
28
|
+
* {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the
|
|
29
|
+
* generation at mount time and skips its `unload()` call if the generation has
|
|
30
|
+
* since advanced — i.e. a new mount already reloaded the script before the old
|
|
31
|
+
* mount's delayed cleanup ran (a page-navigation remount, not a real teardown).
|
|
32
|
+
* `ScriptLoader`'s own ref-counting handles the common case; this guards the
|
|
33
|
+
* one case it doesn't: `reload()` doesn't change the reference count, so a
|
|
34
|
+
* stale `unload()` after a `reload()` could drop the count to zero and tear
|
|
35
|
+
* down a script a fresh mount is now depending on.
|
|
36
|
+
*/
|
|
37
|
+
const generationByVariant = /* @__PURE__ */ new Map();
|
|
38
|
+
function bumpGeneration(variant) {
|
|
39
|
+
const next = (generationByVariant.get(variant) ?? 0) + 1;
|
|
40
|
+
generationByVariant.set(variant, next);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
43
|
+
function currentGeneration(variant) {
|
|
44
|
+
return generationByVariant.get(variant) ?? 0;
|
|
45
|
+
}
|
|
46
|
+
const DEFAULT_LEADCAPTURE_SCRIPT_URL = "https://api.useleadbot.com/lead-bots/get-pixel-script.js";
|
|
47
|
+
/**
|
|
48
|
+
* LeadCaptureForm — renders a LeadCapture IO form with support for
|
|
49
|
+
* configurable variants, built on `@silverassist/next-script-loader`'s
|
|
50
|
+
* singleton, reference-counted, ownership-arbitrated script lifecycle.
|
|
51
|
+
*
|
|
52
|
+
* A single form can render in multiple DOM locations via the
|
|
53
|
+
* `.leadforms-embd-form` class — LeadCapture IO's script populates every
|
|
54
|
+
* matching div once it loads, it doesn't re-run per div.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```tsx
|
|
58
|
+
* // Modal usage
|
|
59
|
+
* <LeadCaptureForm
|
|
60
|
+
* formVariant="desktop"
|
|
61
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
62
|
+
* usageContext="modal"
|
|
63
|
+
* isModalOpen={isOpen}
|
|
64
|
+
* />
|
|
65
|
+
*
|
|
66
|
+
* // On-page usage
|
|
67
|
+
* <LeadCaptureForm
|
|
68
|
+
* formVariant="mobile"
|
|
69
|
+
* formTokens={{ desktop: "GLFT-XXXX", mobile: "GLFT-YYYY" }}
|
|
70
|
+
* usageContext="onPage"
|
|
71
|
+
* isModalOpen={true}
|
|
72
|
+
* />
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
function LeadCaptureForm({ formVariant, formTokens, scriptUrl, usageContext, isModalOpen, className = "", embedTargetId }) {
|
|
76
|
+
const [isInViewport, setIsInViewport] = useState(false);
|
|
77
|
+
const formRef = useRef(null);
|
|
78
|
+
const mountGenRef = useRef(0);
|
|
79
|
+
const isMountedRef = useRef(true);
|
|
80
|
+
const containerId = `leadcapture-container-${formVariant}-${usageContext}`;
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
isMountedRef.current = true;
|
|
83
|
+
return () => {
|
|
84
|
+
isMountedRef.current = false;
|
|
85
|
+
};
|
|
86
|
+
}, []);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
leadCaptureLoader.configure({ urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL } });
|
|
89
|
+
}, [formVariant, scriptUrl]);
|
|
90
|
+
/**
|
|
91
|
+
* Intersection Observer for onPage forms — loads when near viewport.
|
|
92
|
+
*/
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (usageContext !== "onPage" || !formRef.current) return;
|
|
95
|
+
const observer = new IntersectionObserver((entries) => {
|
|
96
|
+
entries.forEach((entry) => {
|
|
97
|
+
if (entry.isIntersecting) {
|
|
98
|
+
setIsInViewport(true);
|
|
99
|
+
observer.disconnect();
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}, {
|
|
103
|
+
rootMargin: "100px",
|
|
104
|
+
threshold: .1
|
|
105
|
+
});
|
|
106
|
+
observer.observe(formRef.current);
|
|
107
|
+
return () => observer.disconnect();
|
|
108
|
+
}, [usageContext]);
|
|
109
|
+
/**
|
|
110
|
+
* Script loading with minimal interaction pattern.
|
|
111
|
+
* - Modal: loads immediately when the modal opens.
|
|
112
|
+
* - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once
|
|
113
|
+
* near viewport.
|
|
114
|
+
*/
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (!(usageContext === "modal" ? isModalOpen === true : isInViewport)) return;
|
|
117
|
+
const load = () => {
|
|
118
|
+
window.form_token = formTokens[formVariant];
|
|
119
|
+
bumpGeneration(formVariant);
|
|
120
|
+
leadCaptureLoader.load(formVariant).then(() => {
|
|
121
|
+
if (!isMountedRef.current) return;
|
|
122
|
+
leadCaptureLoader.forceSetOwner(containerId);
|
|
123
|
+
}).catch(() => {});
|
|
124
|
+
};
|
|
125
|
+
if (usageContext === "modal") {
|
|
126
|
+
load();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const events = [
|
|
130
|
+
"focus",
|
|
131
|
+
"mousemove",
|
|
132
|
+
"scroll",
|
|
133
|
+
"touchstart"
|
|
134
|
+
];
|
|
135
|
+
const loadOnce = () => {
|
|
136
|
+
load();
|
|
137
|
+
events.forEach((event) => document.removeEventListener(event, loadOnce));
|
|
138
|
+
};
|
|
139
|
+
events.forEach((event) => {
|
|
140
|
+
document.addEventListener(event, loadOnce, { once: true });
|
|
141
|
+
});
|
|
142
|
+
return () => {
|
|
143
|
+
events.forEach((event) => document.removeEventListener(event, loadOnce));
|
|
144
|
+
};
|
|
145
|
+
}, [
|
|
146
|
+
isModalOpen,
|
|
147
|
+
isInViewport,
|
|
148
|
+
formVariant,
|
|
149
|
+
usageContext,
|
|
150
|
+
containerId,
|
|
151
|
+
formTokens
|
|
152
|
+
]);
|
|
153
|
+
/**
|
|
154
|
+
* Capture the current generation at mount time — passed to the delayed
|
|
155
|
+
* unload on cleanup so a stale unmount (superseded by a fresh mount that
|
|
156
|
+
* already reloaded) is a no-op.
|
|
157
|
+
*/
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
mountGenRef.current = currentGeneration(formVariant);
|
|
160
|
+
}, [formVariant]);
|
|
161
|
+
/**
|
|
162
|
+
* Cleanup on unmount only.
|
|
163
|
+
* Modal: only releases ownership, doesn't unload the script.
|
|
164
|
+
* OnPage: releases ownership and unloads after a short delay, skipped if
|
|
165
|
+
* a newer mount has already reloaded the script in the meantime.
|
|
166
|
+
*/
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
return () => {
|
|
169
|
+
leadCaptureLoader.releaseOwnership(containerId);
|
|
170
|
+
if (usageContext === "onPage") {
|
|
171
|
+
const gen = mountGenRef.current;
|
|
172
|
+
setTimeout(() => {
|
|
173
|
+
if (gen < currentGeneration(formVariant)) return;
|
|
174
|
+
leadCaptureLoader.unload();
|
|
175
|
+
}, 100);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}, []);
|
|
179
|
+
return /* @__PURE__ */ jsx("div", {
|
|
180
|
+
ref: formRef,
|
|
181
|
+
id: containerId,
|
|
182
|
+
className,
|
|
183
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
184
|
+
className: "leadforms-embd-form",
|
|
185
|
+
...embedTargetId ? { id: embedTargetId } : {}
|
|
186
|
+
})
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
export { LeadCaptureForm as default, leadCaptureLoader };
|
|
192
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.tsx"],"sourcesContent":["/**\n * @packageDocumentation\n * LeadCapture IO form integration for Next.js — a variant-switching,\n * ownership-arbitrated `LeadCaptureForm` component built on\n * `@silverassist/next-script-loader`.\n */\n\n\"use client\";\n\nimport { ScriptLoader } from \"@silverassist/next-script-loader\";\nimport { useEffect, useRef, useState } from \"react\";\n\nexport type UsageContext = \"modal\" | \"onPage\";\n\n/**\n * Module-level singleton: every `LeadCaptureForm` instance on the page\n * shares one loader. `ScriptLoader` tracks a single active variant at a\n * time — switching variants tears down the previous one — which matches\n * how this form is actually used in the fleet (one device/territory\n * variant active per page). A page that genuinely needs two different\n * variants loaded simultaneously (e.g. a modal on \"desktop\" and an on-page\n * form on \"mobile\" at once) isn't supported by this shared instance; see\n * the README for the workaround.\n */\nexport const leadCaptureLoader = new ScriptLoader();\n\n/**\n * Generation counter per variant, incremented on every {@link ScriptLoader.load}/\n * {@link ScriptLoader.reload} call. A delayed on-unmount cleanup captures the\n * generation at mount time and skips its `unload()` call if the generation has\n * since advanced — i.e. a new mount already reloaded the script before the old\n * mount's delayed cleanup ran (a page-navigation remount, not a real teardown).\n * `ScriptLoader`'s own ref-counting handles the common case; this guards the\n * one case it doesn't: `reload()` doesn't change the reference count, so a\n * stale `unload()` after a `reload()` could drop the count to zero and tear\n * down a script a fresh mount is now depending on.\n */\nconst generationByVariant = new Map<string, number>();\n\nfunction bumpGeneration(variant: string): number {\n const next = (generationByVariant.get(variant) ?? 0) + 1;\n generationByVariant.set(variant, next);\n return next;\n}\n\nfunction currentGeneration(variant: string): number {\n return generationByVariant.get(variant) ?? 0;\n}\n\nexport interface LeadCaptureFormProps {\n /**\n * Form variant to render (e.g. \"desktop\", \"mobile\", \"itt\", \"oot\"). Must\n * match a key configured in `formTokens`.\n */\n formVariant: string;\n\n /** Map of variant names to LeadCapture IO form tokens. */\n formTokens: Record<string, string>;\n\n /** Script URL override, if not using LeadCapture IO's default CDN. */\n scriptUrl?: string;\n\n /**\n * Usage context — determines loading behavior.\n * - `modal`: loads when the modal opens\n * - `onPage`: loads when in viewport, after minimal interaction\n */\n usageContext: UsageContext;\n\n /** Controls whether the modal is open (only relevant for `usageContext=\"modal\"`). */\n isModalOpen: boolean;\n\n /** Optional additional CSS classes. */\n className?: string;\n\n /**\n * Optional embed target id for the inner `.leadforms-embd-form` div. Must\n * match the WordPress `embed_target_id` when the site sources form\n * placement from WordPress.\n */\n embedTargetId?: string;\n}\n\nconst DEFAULT_LEADCAPTURE_SCRIPT_URL = \"https://api.useleadbot.com/lead-bots/get-pixel-script.js\";\n\n/**\n * LeadCaptureForm — renders a LeadCapture IO form with support for\n * configurable variants, built on `@silverassist/next-script-loader`'s\n * singleton, reference-counted, ownership-arbitrated script lifecycle.\n *\n * A single form can render in multiple DOM locations via the\n * `.leadforms-embd-form` class — LeadCapture IO's script populates every\n * matching div once it loads, it doesn't re-run per div.\n *\n * @example\n * ```tsx\n * // Modal usage\n * <LeadCaptureForm\n * formVariant=\"desktop\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"modal\"\n * isModalOpen={isOpen}\n * />\n *\n * // On-page usage\n * <LeadCaptureForm\n * formVariant=\"mobile\"\n * formTokens={{ desktop: \"GLFT-XXXX\", mobile: \"GLFT-YYYY\" }}\n * usageContext=\"onPage\"\n * isModalOpen={true}\n * />\n * ```\n */\nexport default function LeadCaptureForm({\n formVariant,\n formTokens,\n scriptUrl,\n usageContext,\n isModalOpen,\n className = \"\",\n embedTargetId,\n}: LeadCaptureFormProps) {\n const [isInViewport, setIsInViewport] = useState(false);\n const formRef = useRef<HTMLDivElement>(null);\n const mountGenRef = useRef<number>(0);\n const isMountedRef = useRef<boolean>(true);\n const containerId = `leadcapture-container-${formVariant}-${usageContext}`;\n\n useEffect(() => {\n isMountedRef.current = true;\n return () => {\n isMountedRef.current = false;\n };\n }, []);\n\n useEffect(() => {\n leadCaptureLoader.configure({\n urls: { [formVariant]: scriptUrl ?? DEFAULT_LEADCAPTURE_SCRIPT_URL },\n });\n }, [formVariant, scriptUrl]);\n\n /**\n * Intersection Observer for onPage forms — loads when near viewport.\n */\n useEffect(() => {\n if (usageContext !== \"onPage\" || !formRef.current) return;\n\n const observer = new IntersectionObserver(\n (entries) => {\n entries.forEach((entry) => {\n if (entry.isIntersecting) {\n setIsInViewport(true);\n observer.disconnect();\n }\n });\n },\n { rootMargin: \"100px\", threshold: 0.1 },\n );\n\n observer.observe(formRef.current);\n return () => observer.disconnect();\n }, [usageContext]);\n\n /**\n * Script loading with minimal interaction pattern.\n * - Modal: loads immediately when the modal opens.\n * - OnPage: loads on the first of focus/mousemove/scroll/touchstart, once\n * near viewport.\n */\n useEffect(() => {\n const shouldLoad = usageContext === \"modal\" ? isModalOpen === true : isInViewport;\n\n if (!shouldLoad) return;\n\n const load = () => {\n // LeadCapture IO serves one shared script for every variant and reads\n // which form to render from a global set just before the script\n // loads, rather than varying the script URL itself per variant.\n (window as Window & { form_token?: string }).form_token = formTokens[formVariant];\n\n bumpGeneration(formVariant);\n leadCaptureLoader\n .load(formVariant)\n .then(() => {\n if (!isMountedRef.current) return;\n leadCaptureLoader.forceSetOwner(containerId);\n })\n .catch(() => {\n // Silently degrade — the surrounding page stays usable without\n // the embed.\n });\n };\n\n if (usageContext === \"modal\") {\n load();\n return;\n }\n\n const events = [\"focus\", \"mousemove\", \"scroll\", \"touchstart\"] as const;\n const loadOnce = () => {\n load();\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n events.forEach((event) => {\n document.addEventListener(event, loadOnce, { once: true });\n });\n\n return () => {\n events.forEach((event) => document.removeEventListener(event, loadOnce));\n };\n }, [isModalOpen, isInViewport, formVariant, usageContext, containerId, formTokens]);\n\n /**\n * Capture the current generation at mount time — passed to the delayed\n * unload on cleanup so a stale unmount (superseded by a fresh mount that\n * already reloaded) is a no-op.\n */\n useEffect(() => {\n mountGenRef.current = currentGeneration(formVariant);\n }, [formVariant]);\n\n /**\n * Cleanup on unmount only.\n * Modal: only releases ownership, doesn't unload the script.\n * OnPage: releases ownership and unloads after a short delay, skipped if\n * a newer mount has already reloaded the script in the meantime.\n */\n useEffect(() => {\n return () => {\n leadCaptureLoader.releaseOwnership(containerId);\n\n if (usageContext === \"onPage\") {\n const gen = mountGenRef.current;\n setTimeout(() => {\n if (gen < currentGeneration(formVariant)) return;\n leadCaptureLoader.unload();\n }, 100);\n }\n };\n }, []);\n\n return (\n <div ref={formRef} id={containerId} className={className}>\n {/*\n LeadCapture IO embed container.\n\n CRITICAL: this div must exist BEFORE the script loads -- LeadCapture IO\n only populates `.leadforms-embd-form` divs present at load time, it\n doesn't detect ones added later.\n */}\n <div className=\"leadforms-embd-form\" {...(embedTargetId ? { id: embedTargetId } : {})}>\n {/* Form renders here */}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@silverassist/leadcapture-form",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "LeadCapture IO form integration for Next.js — variant switching, ownership arbitration, and ref-counted script lifecycle via @silverassist/next-script-loader",
|
|
5
|
+
"author": "Miguel Colmenares <me@miguelcolmenares.com>",
|
|
6
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
7
|
+
"prettier": "@silverassist/npm-package-standards/prettier",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/SilverAssist/next-leadcapture-form.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/SilverAssist/next-leadcapture-form/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/SilverAssist/next-leadcapture-form#readme",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"leadcapture",
|
|
21
|
+
"lead-forms",
|
|
22
|
+
"nextjs",
|
|
23
|
+
"next",
|
|
24
|
+
"react",
|
|
25
|
+
"script-loader"
|
|
26
|
+
],
|
|
27
|
+
"main": "./dist/index.js",
|
|
28
|
+
"module": "./dist/index.mjs",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"import": {
|
|
33
|
+
"types": "./dist/index.d.mts",
|
|
34
|
+
"default": "./dist/index.mjs"
|
|
35
|
+
},
|
|
36
|
+
"require": {
|
|
37
|
+
"types": "./dist/index.d.ts",
|
|
38
|
+
"default": "./dist/index.js"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"LICENSE",
|
|
47
|
+
"CHANGELOG.md"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsdown",
|
|
51
|
+
"dev": "tsdown --watch",
|
|
52
|
+
"test": "jest",
|
|
53
|
+
"test:watch": "jest --watch",
|
|
54
|
+
"test:coverage": "jest --coverage",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"lint": "eslint .",
|
|
57
|
+
"lint:fix": "eslint . --fix",
|
|
58
|
+
"format": "prettier --write \"**/*.{js,mjs,ts,tsx,json,md}\"",
|
|
59
|
+
"format:check": "prettier --check \"**/*.{js,mjs,ts,tsx,json,md}\"",
|
|
60
|
+
"clean": "rm -rf dist",
|
|
61
|
+
"prepublishOnly": "npm run clean && npm run check && npm run build",
|
|
62
|
+
"release": "npm publish --access public",
|
|
63
|
+
"check": "npm run format:check && npm run typecheck && npm run lint && npm run test",
|
|
64
|
+
"prepare": "husky || true",
|
|
65
|
+
"e2e:setup": "next-testing-toolkit build-fixture --port 3218",
|
|
66
|
+
"e2e": "npm run e2e:setup && playwright test -c e2e/playwright.config.ts"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"next": ">=16.0.0",
|
|
70
|
+
"react": ">=18.0.0",
|
|
71
|
+
"react-dom": ">=18.0.0"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"@silverassist/next-script-loader": "^0.1.0"
|
|
75
|
+
},
|
|
76
|
+
"devDependencies": {
|
|
77
|
+
"@playwright/test": "^1.62.1",
|
|
78
|
+
"@silverassist/next-testing-toolkit": "^0.2.1",
|
|
79
|
+
"@silverassist/npm-package-standards": "^0.1.0",
|
|
80
|
+
"@swc/core": "^1.16.1",
|
|
81
|
+
"@swc/jest": "^0.2.39",
|
|
82
|
+
"@testing-library/jest-dom": "^7.0.0",
|
|
83
|
+
"@testing-library/react": "^16.3.2",
|
|
84
|
+
"@types/jest": "^30.0.0",
|
|
85
|
+
"@types/node": "^26.0.0",
|
|
86
|
+
"@types/react": "^19.2.9",
|
|
87
|
+
"@types/react-dom": "^19.2.3",
|
|
88
|
+
"eslint": "^9.39.1",
|
|
89
|
+
"eslint-plugin-react": "^7.37.5",
|
|
90
|
+
"globals": "^17.11.0",
|
|
91
|
+
"husky": "^9.1.7",
|
|
92
|
+
"jest": "^30.2.0",
|
|
93
|
+
"jest-environment-jsdom": "^30.2.0",
|
|
94
|
+
"lint-staged": "^17.3.0",
|
|
95
|
+
"next": "^16.1.5",
|
|
96
|
+
"prettier": "^3.6.2",
|
|
97
|
+
"react": "^19.2.4",
|
|
98
|
+
"react-dom": "^19.2.4",
|
|
99
|
+
"tsdown": "^0.22.14",
|
|
100
|
+
"typescript": "^6.0.3",
|
|
101
|
+
"typescript-eslint": "^8.68.0"
|
|
102
|
+
},
|
|
103
|
+
"engines": {
|
|
104
|
+
"node": ">=22.0.0"
|
|
105
|
+
},
|
|
106
|
+
"lint-staged": {
|
|
107
|
+
"*.{ts,tsx,js,mjs,cjs,json,md}": [
|
|
108
|
+
"prettier --write"
|
|
109
|
+
],
|
|
110
|
+
"*.{ts,tsx}": [
|
|
111
|
+
"eslint --fix"
|
|
112
|
+
]
|
|
113
|
+
}
|
|
114
|
+
}
|