devx-web-widget 1.0.0 → 1.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/README.md +30 -91
- package/dist/feedback-widget.js +309 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +737 -0
- package/dist/index.js.map +1 -0
- package/dist/index.min.js +6 -0
- package/package.json +1 -1
- package/src/feedback-widget.js +0 -496
package/README.md
CHANGED
|
@@ -4,20 +4,15 @@ A small, dependency-free feedback widget you can drop into any website. It provi
|
|
|
4
4
|
- "Feedback on this page" — pick an element on the page and include a selector in the feedback
|
|
5
5
|
- "Quick feedback" — general feedback (selector hidden)
|
|
6
6
|
|
|
7
|
-
**Features**
|
|
8
|
-
- **Style-isolated**: runs inside a Shadow DOM to avoid style collisions
|
|
9
|
-
**DevX Web Feedback Widget**
|
|
10
|
-
|
|
11
|
-
A small, dependency-free feedback widget you can drop into any website. It provides a side tab that opens two feedback flows:
|
|
12
|
-
- "Feedback on this page" — pick an element on the page and include a selector in the feedback
|
|
13
|
-
- "Quick feedback" — general feedback (selector hidden)
|
|
14
|
-
|
|
15
7
|
**Features**
|
|
16
8
|
- **Style-isolated**: runs inside a Shadow DOM to avoid style collisions
|
|
17
9
|
- **Two flows**: element picker and quick feedback
|
|
18
|
-
- **Lightweight**: no runtime dependencies for the
|
|
10
|
+
- **Lightweight**: no runtime dependencies for the module
|
|
19
11
|
- **Event-driven**: emits a `feedbackwidget:submit` event with the payload
|
|
20
12
|
|
|
13
|
+
**About DevX**
|
|
14
|
+
DevX is a platform for developer experience tooling and integrations — learn more at https://devx.today/. This repository provides the official web feedback widget used by DevX for collecting lightweight feedback from web pages.
|
|
15
|
+
|
|
21
16
|
**Install (npm / pnpm / yarn)**
|
|
22
17
|
Install the package from the registry (package name from `package.json` is `devx-web-widget`):
|
|
23
18
|
|
|
@@ -33,22 +28,37 @@ yarn add devx-web-widget
|
|
|
33
28
|
```
|
|
34
29
|
|
|
35
30
|
**Quick usage — Browser script (drop-in)**
|
|
36
|
-
|
|
31
|
+
|
|
32
|
+
CDN (recommended for quick start):
|
|
33
|
+
|
|
34
|
+
Include the prebuilt CDN-hosted bundle directly from unpkg:
|
|
37
35
|
|
|
38
36
|
```html
|
|
37
|
+
<script src="https://app.unpkg.com/devx-web-widget@1.1.0/files/dist/feedback-widget.js"></script>
|
|
39
38
|
<script>
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
|
|
40
|
+
const DEFAULT_CONFIG = {
|
|
41
|
+
buttonLabel: 'Feedback',
|
|
42
|
+
backgroundColor: '#111827',
|
|
43
|
+
textColor: '#ffffff',
|
|
44
|
+
accentColor: '#2F6FED',
|
|
45
|
+
font: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',
|
|
46
|
+
position: 'right',
|
|
47
|
+
widgetType: 'default'
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const feedbackWidget = new FeedbackWidget("api_key", DEFAULT_CONFIG);
|
|
51
|
+
|
|
52
|
+
window.addEventListener('feedbackwidget:submit', (event) => {
|
|
53
|
+
console.log(event)
|
|
54
|
+
});
|
|
55
|
+
</script>
|
|
47
56
|
```
|
|
48
57
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
Note: using the CDN is the simplest way to get started; it serves the built `dist/feedback-widget.js` for versioned consumption.
|
|
59
|
+
|
|
60
|
+
Local bundle (if you need to customize or avoid CDN):
|
|
61
|
+
|
|
52
62
|
|
|
53
63
|
**Usage — npm package / ES module (DevX projects)**
|
|
54
64
|
Import the shipped module and instantiate the widget in an ESM environment (recommended for DevX apps):
|
|
@@ -86,43 +96,6 @@ Pass the config as the second constructor argument (or use `window.FeedbackWidge
|
|
|
86
96
|
- `position` (`left` | `right`)
|
|
87
97
|
- `widgetType` (`default` | `chatbot`)
|
|
88
98
|
|
|
89
|
-
**API / endpoint and API Key**
|
|
90
|
-
- Browser build: set `window.FeedbackWidgetConfig.endpoint` to have the widget POST the submitted payload directly to your endpoint.
|
|
91
|
-
- npm/TS class: passing a non-empty API key to `new FeedbackWidget(apiKey, config)` causes the widget to POST a formatted message body to `https://devx.today/v1/widget/ingest` with `Authorization: Bearer <API_KEY>`.
|
|
92
|
-
|
|
93
|
-
Security recommendations (for DevX teams)
|
|
94
|
-
- Do not embed production API keys in client bundles. Instead use a server-side ingestion proxy:
|
|
95
|
-
1. Client widget posts to your internal endpoint (no secret in client)
|
|
96
|
-
2. Your server validates, augments, and forwards to DevX ingestion (`https://devx.today/v1/widget/ingest`) using a server-side-stored API key
|
|
97
|
-
|
|
98
|
-
Example Node/Express forwarder (DevX pattern):
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
// server.js
|
|
102
|
-
import express from 'express';
|
|
103
|
-
import fetch from 'node-fetch';
|
|
104
|
-
|
|
105
|
-
const app = express();
|
|
106
|
-
app.use(express.json());
|
|
107
|
-
|
|
108
|
-
app.post('/api/widget-feedback', async (req, res) => {
|
|
109
|
-
const payload = req.body;
|
|
110
|
-
// perform validation, rate-limiting, enrich payload as needed
|
|
111
|
-
await fetch('https://devx.today/v1/widget/ingest', {
|
|
112
|
-
method: 'POST',
|
|
113
|
-
headers: {
|
|
114
|
-
'Content-Type': 'application/json',
|
|
115
|
-
'Authorization': `Bearer ${process.env.DEVX_API_KEY}`
|
|
116
|
-
},
|
|
117
|
-
body: JSON.stringify(payload)
|
|
118
|
-
});
|
|
119
|
-
res.status(202).end();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
app.listen(3000);
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Then configure the client/browser widget to use `/api/widget-feedback` as `endpoint`.
|
|
126
99
|
|
|
127
100
|
**Event payload**
|
|
128
101
|
The widget emits `feedbackwidget:submit` with `event.detail` containing:
|
|
@@ -130,37 +103,3 @@ The widget emits `feedbackwidget:submit` with `event.detail` containing:
|
|
|
130
103
|
- `optionType`: `page_item_visible` | `page_item_hidden`
|
|
131
104
|
- `elementSelector`: CSS selector string or `null`
|
|
132
105
|
- `pageUrl`, `pageTitle`, `title`, `feedback`, `email`, `submittedAt`
|
|
133
|
-
|
|
134
|
-
**Build & development**
|
|
135
|
-
- Install dependencies (pnpm recommended):
|
|
136
|
-
|
|
137
|
-
```bash
|
|
138
|
-
pnpm install
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
- Dev (live watch):
|
|
142
|
-
|
|
143
|
-
```bash
|
|
144
|
-
pnpm run dev
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
- Build to `dist`:
|
|
148
|
-
|
|
149
|
-
```bash
|
|
150
|
-
pnpm run build
|
|
151
|
-
```
|
|
152
|
-
|
|
153
|
-
**Publishing notes (npm / DevX registry)**
|
|
154
|
-
- Verify `package.json.name` is `devx-web-widget` and update `version`, `license`, and `repository` fields.
|
|
155
|
-
- If publishing to an internal DevX registry, update `.npmrc` with the registry authentication and scope as required.
|
|
156
|
-
|
|
157
|
-
**Contributing / Extending**
|
|
158
|
-
- To add a configurable `endpoint` to the TypeScript constructor I can implement that change and update tests.
|
|
159
|
-
- Consider adding unit tests for selector generation, and an optional E2E demo page for QA.
|
|
160
|
-
|
|
161
|
-
---
|
|
162
|
-
|
|
163
|
-
If you'd like, I can implement any of the following now:
|
|
164
|
-
- add `endpoint` to the TypeScript constructor (so consumers can pass a custom endpoint from code),
|
|
165
|
-
- produce a UMD build for CDN consumption,
|
|
166
|
-
- create a small demo HTML page that loads the built file and demonstrates submissions.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";var FeedbackWidget=(()=>{var c=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var h=Object.getOwnPropertyNames;var f=Object.prototype.hasOwnProperty;var u=(r,e)=>{for(var t in e)c(r,t,{get:e[t],enumerable:!0})},m=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of h(e))!f.call(r,o)&&o!==t&&c(r,o,{get:()=>e[o],enumerable:!(i=p(e,o))||i.enumerable});return r};var g=r=>m(c({},"__esModule",{value:!0}),r);var w={};u(w,{FeedbackWidget:()=>b});var v={buttonLabel:"Feedback",backgroundColor:"#111827",textColor:"#ffffff",accentColor:"#2F6FED",font:'-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif',position:"right",widgetType:"default"},b=class{config;host;root;elements;state;showItemInForm;selectedSelector;selectedEl;hoverRafId;hostId;apiKey;endPoint;constructor(e=null,t={}){this.config={...v,...t},this.hostId=`feedback-widget-host-${Math.random().toString(36).substr(2,9)}`,this.state="idle",this.showItemInForm=!0,this.selectedSelector=null,this.selectedEl=null,this.hoverRafId=null,this.apiKey=e||"",this.endPoint="https://devx.today/v1/widget/ingest",this.initialize()}initialize(){this.createHostAndShadow(),this.injectStyles(),this.createMarkup(),this.cacheElements(),this.attachEventListeners()}createHostAndShadow(){this.host=document.createElement("div"),this.host.id=this.hostId,document.documentElement.appendChild(this.host),this.root=this.host.attachShadow({mode:"open"})}injectStyles(){let e=this.generateCSS(),t=document.createElement("style");t.textContent=e,this.root.appendChild(t)}generateCSS(){let{backgroundColor:e,textColor:t,accentColor:i,font:o,position:n}=this.config,a=this.hexToRgba(i,.22),l=this.hexToRgba(i,.15),s=n==="right"?"translateY(-50%) rotate(180deg)":"translateY(-50%)",d=n==="right"?"8px 0 0 8px":"0 8px 8px 0",x=n==="right"?"0":"auto",y=n==="left"?"0":"auto";return`
|
|
2
|
+
:host { all: initial; }
|
|
3
|
+
* { box-sizing: border-box; font-family: ${o}; }
|
|
4
|
+
|
|
5
|
+
.fbw-tab {
|
|
6
|
+
position: fixed;
|
|
7
|
+
top: 50%;
|
|
8
|
+
${n==="right"?"right":"left"}: 0;
|
|
9
|
+
transform: ${s};
|
|
10
|
+
background: ${e};
|
|
11
|
+
color: ${t};
|
|
12
|
+
writing-mode: vertical-rl;
|
|
13
|
+
text-orientation: mixed;
|
|
14
|
+
padding: 14px 9px;
|
|
15
|
+
border-radius: ${d};
|
|
16
|
+
cursor: pointer;
|
|
17
|
+
font-size: 13px;
|
|
18
|
+
font-weight: 600;
|
|
19
|
+
letter-spacing: 0.03em;
|
|
20
|
+
box-shadow: ${n==="right"?"-2px":"2px"} 0 10px rgba(0,0,0,0.18);
|
|
21
|
+
z-index: 2147483000;
|
|
22
|
+
border: none;
|
|
23
|
+
user-select: none;
|
|
24
|
+
transition: padding 0.12s ease, background 0.12s ease;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.fbw-tab:hover { padding-right: 13px; background: #1f2937; }
|
|
28
|
+
.fbw-tab:focus-visible { outline: 2px solid ${i}; outline-offset: 2px; }
|
|
29
|
+
|
|
30
|
+
.fbw-menu {
|
|
31
|
+
position: fixed;
|
|
32
|
+
top: 50%;
|
|
33
|
+
right: ${n==="right"?"46px":"auto"};
|
|
34
|
+
left: ${n==="left"?"46px":"auto"};
|
|
35
|
+
transform: translateY(-50%);
|
|
36
|
+
background: #fff;
|
|
37
|
+
border-radius: 10px;
|
|
38
|
+
box-shadow: 0 8px 30px rgba(0,0,0,0.22);
|
|
39
|
+
padding: 6px;
|
|
40
|
+
min-width: 220px;
|
|
41
|
+
z-index: 2147483001;
|
|
42
|
+
display: none;
|
|
43
|
+
border: 1px solid #e5e7eb;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.fbw-menu.fbw-open { display: block; }
|
|
47
|
+
|
|
48
|
+
.fbw-menu button {
|
|
49
|
+
display: block;
|
|
50
|
+
width: 100%;
|
|
51
|
+
text-align: left;
|
|
52
|
+
background: none;
|
|
53
|
+
border: none;
|
|
54
|
+
padding: 10px 12px;
|
|
55
|
+
font-size: 13.5px;
|
|
56
|
+
color: #111827;
|
|
57
|
+
border-radius: 6px;
|
|
58
|
+
cursor: pointer;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.fbw-menu button:hover { background: #f3f4f6; }
|
|
62
|
+
.fbw-menu button small { display: block; color: #6b7280; font-weight: 400; margin-top: 2px; font-size: 11.5px; }
|
|
63
|
+
|
|
64
|
+
.fbw-banner {
|
|
65
|
+
position: fixed;
|
|
66
|
+
top: 18px;
|
|
67
|
+
left: 50%;
|
|
68
|
+
transform: translateX(-50%);
|
|
69
|
+
background: ${e};
|
|
70
|
+
color: ${t};
|
|
71
|
+
padding: 10px 10px 10px 16px;
|
|
72
|
+
border-radius: 999px;
|
|
73
|
+
font-size: 13px;
|
|
74
|
+
display: none;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 10px;
|
|
77
|
+
z-index: 2147483002;
|
|
78
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.25);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.fbw-banner.fbw-open { display: flex; }
|
|
82
|
+
|
|
83
|
+
.fbw-banner button {
|
|
84
|
+
background: rgba(255,255,255,0.12);
|
|
85
|
+
color: ${t};
|
|
86
|
+
border: none;
|
|
87
|
+
padding: 6px 12px;
|
|
88
|
+
border-radius: 999px;
|
|
89
|
+
font-size: 12px;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.fbw-banner button:hover { background: rgba(255,255,255,0.22); }
|
|
94
|
+
|
|
95
|
+
.fbw-highlight {
|
|
96
|
+
position: fixed;
|
|
97
|
+
pointer-events: none;
|
|
98
|
+
border: 2px solid ${i};
|
|
99
|
+
box-shadow: 0 0 0 4px ${a};
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
z-index: 2147483000;
|
|
102
|
+
display: none;
|
|
103
|
+
transition: top 0.06s ease, left 0.06s ease, width 0.06s ease, height 0.06s ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.fbw-overlay {
|
|
107
|
+
position: fixed;
|
|
108
|
+
inset: 0;
|
|
109
|
+
background: rgba(17,24,39,0.5);
|
|
110
|
+
display: none;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
z-index: 2147483003;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.fbw-overlay.fbw-open { display: flex; }
|
|
117
|
+
|
|
118
|
+
.fbw-modal {
|
|
119
|
+
background: #fff;
|
|
120
|
+
border-radius: 14px;
|
|
121
|
+
width: 420px;
|
|
122
|
+
max-width: 92vw;
|
|
123
|
+
max-height: 88vh;
|
|
124
|
+
overflow-y: auto;
|
|
125
|
+
padding: 22px;
|
|
126
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.35);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.fbw-modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
|
130
|
+
.fbw-modal-head h2 { font-size: 16px; margin: 0; color: #111827; font-weight: 700; }
|
|
131
|
+
.fbw-close { background: none; border: none; font-size: 18px; color: #6b7280; cursor: pointer; line-height: 1; padding: 4px; }
|
|
132
|
+
.fbw-close:hover { color: #111827; }
|
|
133
|
+
|
|
134
|
+
.fbw-field { margin-bottom: 14px; }
|
|
135
|
+
.fbw-field label { display: block; font-size: 12.5px; font-weight: 600; color: #374151; margin-bottom: 5px; }
|
|
136
|
+
|
|
137
|
+
.fbw-item-box {
|
|
138
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
139
|
+
font-size: 11.5px;
|
|
140
|
+
color: #1f2937;
|
|
141
|
+
background: #f3f4f6;
|
|
142
|
+
border: 1px solid #e5e7eb;
|
|
143
|
+
border-radius: 6px;
|
|
144
|
+
padding: 8px 10px;
|
|
145
|
+
word-break: break-all;
|
|
146
|
+
max-height: 60px;
|
|
147
|
+
overflow-y: auto;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.fbw-field textarea, .fbw-field input[type="email"], .fbw-field input[type="text"] {
|
|
151
|
+
width: 100%;
|
|
152
|
+
border: 1px solid #d1d5db;
|
|
153
|
+
border-radius: 8px;
|
|
154
|
+
padding: 9px 10px;
|
|
155
|
+
font-size: 13.5px;
|
|
156
|
+
color: #111827;
|
|
157
|
+
resize: vertical;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.fbw-field textarea:focus, .fbw-field input:focus {
|
|
161
|
+
outline: none;
|
|
162
|
+
border-color: ${i};
|
|
163
|
+
box-shadow: 0 0 0 3px ${l};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.fbw-error { color: #dc2626; font-size: 12px; margin: -6px 0 12px; display: none; }
|
|
167
|
+
.fbw-error.fbw-show { display: block; }
|
|
168
|
+
|
|
169
|
+
.fbw-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
|
|
170
|
+
|
|
171
|
+
.fbw-btn {
|
|
172
|
+
border: none;
|
|
173
|
+
border-radius: 8px;
|
|
174
|
+
padding: 9px 16px;
|
|
175
|
+
font-size: 13.5px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
cursor: pointer;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.fbw-btn-primary { background: ${i}; color: #fff; }
|
|
181
|
+
.fbw-btn-primary:hover { filter: brightness(1.08); }
|
|
182
|
+
.fbw-btn-ghost { background: none; color: #4b5563; }
|
|
183
|
+
.fbw-btn-ghost:hover { background: #f3f4f6; }
|
|
184
|
+
|
|
185
|
+
.fbw-toast {
|
|
186
|
+
position: fixed;
|
|
187
|
+
bottom: 22px;
|
|
188
|
+
right: 22px;
|
|
189
|
+
background: ${e};
|
|
190
|
+
color: ${t};
|
|
191
|
+
padding: 11px 16px;
|
|
192
|
+
border-radius: 10px;
|
|
193
|
+
font-size: 13px;
|
|
194
|
+
z-index: 2147483004;
|
|
195
|
+
box-shadow: 0 8px 24px rgba(0,0,0,0.3);
|
|
196
|
+
opacity: 0;
|
|
197
|
+
transform: translateY(6px);
|
|
198
|
+
transition: opacity 0.18s ease, transform 0.18s ease;
|
|
199
|
+
pointer-events: none;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.fbw-toast.fbw-show { opacity: 1; transform: translateY(0); }
|
|
203
|
+
.fbw-hidden { display: none !important; }
|
|
204
|
+
`}hexToRgba(e,t){let i=e.replace("#",""),o=i.length===3?i.split("").map(s=>s+s).join(""):i,n=parseInt(o.substring(0,2),16),a=parseInt(o.substring(2,4),16),l=parseInt(o.substring(4,6),16);return`rgba(${n},${a},${l},${t})`}createMarkup(){let e=document.createElement("div");e.innerHTML=this.getMarkup(),this.root.appendChild(e)}getMarkup(){let{buttonLabel:e,widgetType:t}=this.config;return t==="chatbot"?this.getChatbotMarkup(e):this.getDefaultMarkup(e)}getDefaultMarkup(e){return`
|
|
205
|
+
<button class="fbw-tab" type="button" aria-haspopup="true" aria-expanded="false">${this.escapeHtml(e)}</button>
|
|
206
|
+
<div class="fbw-menu" role="menu">
|
|
207
|
+
<button type="button" data-action="pick-visible" role="menuitem">
|
|
208
|
+
Feedback on this page<small>Select any element you have feedback on</small>
|
|
209
|
+
</button>
|
|
210
|
+
<button type="button" data-action="pick-hidden" role="menuitem">
|
|
211
|
+
Quick feedback<small>General feedback on us</small>
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
<div class="fbw-banner">
|
|
215
|
+
<span>Click an element to select it · Esc to cancel</span>
|
|
216
|
+
<button type="button" data-action="cancel-pick">Cancel</button>
|
|
217
|
+
</div>
|
|
218
|
+
<div class="fbw-highlight"></div>
|
|
219
|
+
<div class="fbw-overlay">
|
|
220
|
+
<div class="fbw-modal" role="dialog" aria-modal="true" aria-labelledby="fbw-modal-title">
|
|
221
|
+
<div class="fbw-modal-head">
|
|
222
|
+
<h2 id="fbw-modal-title">Share feedback</h2>
|
|
223
|
+
<button class="fbw-close" type="button" data-action="close-modal" aria-label="Close">×</button>
|
|
224
|
+
</div>
|
|
225
|
+
<form data-role="form">
|
|
226
|
+
<div class="fbw-field" data-role="item-field">
|
|
227
|
+
<label>Selected element</label>
|
|
228
|
+
<div class="fbw-item-box" data-role="item-box"></div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="fbw-field">
|
|
231
|
+
<label for="fbw-title">Title</label>
|
|
232
|
+
<input id="fbw-title" data-role="title" type="text" placeholder="Brief summary of your feedback" required />
|
|
233
|
+
</div>
|
|
234
|
+
<div class="fbw-field">
|
|
235
|
+
<label for="fbw-feedback">What's going on?</label>
|
|
236
|
+
<textarea id="fbw-feedback" data-role="feedback" rows="4" placeholder="Tell us what you noticed..."></textarea>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="fbw-field">
|
|
239
|
+
<label for="fbw-email">Email</label>
|
|
240
|
+
<input id="fbw-email" data-role="email" type="email" placeholder="you@example.com" required />
|
|
241
|
+
</div>
|
|
242
|
+
<div class="fbw-error" data-role="error"></div>
|
|
243
|
+
<div class="fbw-actions">
|
|
244
|
+
<button type="button" class="fbw-btn fbw-btn-ghost" data-action="close-modal">Cancel</button>
|
|
245
|
+
<button type="submit" class="fbw-btn fbw-btn-primary">Submit</button>
|
|
246
|
+
</div>
|
|
247
|
+
</form>
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
<div class="fbw-toast" data-role="toast"></div>
|
|
251
|
+
`}getChatbotMarkup(e){return`
|
|
252
|
+
<button class="fbw-tab" type="button" aria-haspopup="true" aria-expanded="false">${this.escapeHtml(e)}</button>
|
|
253
|
+
<div class="fbw-menu" role="menu">
|
|
254
|
+
<button type="button" data-action="pick-visible" role="menuitem">
|
|
255
|
+
Feedback on this page<small>Select any element you have feedback on</small>
|
|
256
|
+
</button>
|
|
257
|
+
<button type="button" data-action="pick-hidden" role="menuitem">
|
|
258
|
+
Quick feedback<small>General feedback on us</small>
|
|
259
|
+
</button>
|
|
260
|
+
</div>
|
|
261
|
+
<div class="fbw-banner">
|
|
262
|
+
<span>Click an element to select it · Esc to cancel</span>
|
|
263
|
+
<button type="button" data-action="cancel-pick">Cancel</button>
|
|
264
|
+
</div>
|
|
265
|
+
<div class="fbw-highlight"></div>
|
|
266
|
+
<div class="fbw-overlay">
|
|
267
|
+
<div class="fbw-modal" role="dialog" aria-modal="true" aria-labelledby="fbw-modal-title">
|
|
268
|
+
<div class="fbw-modal-head">
|
|
269
|
+
<h2 id="fbw-modal-title">Share feedback</h2>
|
|
270
|
+
<button class="fbw-close" type="button" data-action="close-modal" aria-label="Close">×</button>
|
|
271
|
+
</div>
|
|
272
|
+
<form data-role="form">
|
|
273
|
+
<div class="fbw-field" data-role="item-field">
|
|
274
|
+
<label>Selected element</label>
|
|
275
|
+
<div class="fbw-item-box" data-role="item-box"></div>
|
|
276
|
+
</div>
|
|
277
|
+
<div class="fbw-field">
|
|
278
|
+
<label for="fbw-title">Title</label>
|
|
279
|
+
<input id="fbw-title" data-role="title" type="text" placeholder="Brief summary of your feedback" required />
|
|
280
|
+
</div>
|
|
281
|
+
<div class="fbw-field">
|
|
282
|
+
<label for="fbw-feedback">What's going on?</label>
|
|
283
|
+
<textarea id="fbw-feedback" data-role="feedback" rows="4" placeholder="Tell us what you noticed..."></textarea>
|
|
284
|
+
</div>
|
|
285
|
+
<div class="fbw-field">
|
|
286
|
+
<label for="fbw-email">Email</label>
|
|
287
|
+
<input id="fbw-email" data-role="email" type="email" placeholder="you@example.com" required />
|
|
288
|
+
</div>
|
|
289
|
+
<div class="fbw-error" data-role="error"></div>
|
|
290
|
+
<div class="fbw-actions">
|
|
291
|
+
<button type="button" class="fbw-btn fbw-btn-ghost" data-action="close-modal">Cancel</button>
|
|
292
|
+
<button type="submit" class="fbw-btn fbw-btn-primary">Submit</button>
|
|
293
|
+
</div>
|
|
294
|
+
</form>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
<div class="fbw-toast" data-role="toast"></div>
|
|
298
|
+
`}escapeHtml(e){let t=document.createElement("div");return t.textContent=e,t.innerHTML}cacheElements(){this.elements={tab:this.root.querySelector(".fbw-tab"),menu:this.root.querySelector(".fbw-menu"),banner:this.root.querySelector(".fbw-banner"),highlight:this.root.querySelector(".fbw-highlight"),overlay:this.root.querySelector(".fbw-overlay"),modal:this.root.querySelector(".fbw-modal"),form:this.root.querySelector('[data-role="form"]'),itemField:this.root.querySelector('[data-role="item-field"]'),itemBox:this.root.querySelector('[data-role="item-box"]'),title:this.root.querySelector('[data-role="title"]'),feedback:this.root.querySelector('[data-role="feedback"]'),email:this.root.querySelector('[data-role="email"]'),error:this.root.querySelector('[data-role="error"]'),toast:this.root.querySelector('[data-role="toast"]')}}attachEventListeners(){this.elements.tab.addEventListener("click",()=>this.onTabClick()),this.elements.menu.addEventListener("click",e=>this.onMenuClick(e)),this.elements.banner.addEventListener("click",e=>this.onBannerClick(e)),this.elements.overlay.addEventListener("click",e=>this.onOverlayClick(e)),this.elements.form.addEventListener("submit",e=>this.onSubmit(e))}onTabClick(){this.state==="menu"?this.closeMenu():this.openMenu()}openMenu(){this.state="menu",this.elements.menu.classList.add("fbw-open"),this.elements.tab.setAttribute("aria-expanded","true"),document.addEventListener("click",this.onOutsideMenuClick,!0)}closeMenu(){this.state==="menu"&&(this.state="idle"),this.elements.menu.classList.remove("fbw-open"),this.elements.tab.setAttribute("aria-expanded","false"),document.removeEventListener("click",this.onOutsideMenuClick,!0)}onOutsideMenuClick=e=>{this.isInsideWidget(e.target)||this.closeMenu()};onMenuClick(e){let t=e.target.closest("button[data-action]");if(!t)return;let i=t.getAttribute("data-action");i==="pick-visible"&&this.startPicking(!0),i==="pick-hidden"&&(this.showItemInForm=!1,this.closeMenu(),this.openModal())}startPicking(e){this.showItemInForm=e,this.closeMenu(),this.state="picking",this.elements.banner.classList.add("fbw-open"),document.addEventListener("mousemove",this.onHoverMove,!0),document.addEventListener("click",this.onDocumentClickCapture,!0),document.addEventListener("keydown",this.onKeyDownCapture,!0),document.addEventListener("scroll",this.onScrollOrResize,!0),window.addEventListener("resize",this.onScrollOrResize,!0)}stopPicking(){this.elements.banner.classList.remove("fbw-open"),document.removeEventListener("mousemove",this.onHoverMove,!0),document.removeEventListener("scroll",this.onScrollOrResize,!0),window.removeEventListener("resize",this.onScrollOrResize,!0)}onBannerClick(e){e.target.closest('[data-action="cancel-pick"]')&&this.cancelAll()}onHoverMove=e=>{this.isInsideWidget(e.target)||(this.hoverRafId&&cancelAnimationFrame(this.hoverRafId),this.hoverRafId=requestAnimationFrame(()=>{this.positionHighlight(e.target)}))};positionHighlight(e){let t=e.getBoundingClientRect();this.elements.highlight.style.display="block",this.elements.highlight.style.top=`${t.top}px`,this.elements.highlight.style.left=`${t.left}px`,this.elements.highlight.style.width=`${t.width}px`,this.elements.highlight.style.height=`${t.height}px`}onScrollOrResize=()=>{this.selectedEl&&this.state==="review"&&this.positionHighlight(this.selectedEl)};onDocumentClickCapture=e=>{this.isInsideWidget(e.target)||(e.preventDefault(),e.stopPropagation(),typeof e.stopImmediatePropagation=="function"&&e.stopImmediatePropagation(),this.state==="picking"&&this.finalizeSelection(e.target))};onKeyDownCapture=e=>{e.key==="Escape"&&(e.preventDefault(),this.cancelAll())};finalizeSelection(e){this.selectedEl=e,this.selectedSelector=this.getUniqueSelector(e),this.stopPicking(),this.positionHighlight(e),this.openModal()}getUniqueSelector(e){if(!(e instanceof Element))return null;if(e.id){let o=`#${this.cssEscape(e.id)}`;if(this.safeQueryCount(o)===1)return o}let t=[],i=e;for(;i&&i.nodeType===1&&i!==document.documentElement;){let o=i.tagName.toLowerCase();if(i.id){o=`#${this.cssEscape(i.id)}`,t.unshift(o);break}let n=Array.from(i.classList).filter(s=>!!s);n.length&&(o+="."+n.map(this.cssEscape).join("."));let a=i.parentElement;if(a){let s=Array.from(a.children).filter(d=>d.tagName===i.tagName);if(s.length>1){let d=s.indexOf(i)+1;o+=`:nth-of-type(${d})`}}t.unshift(o);let l=t.join(" > ");if(this.safeQueryCount(l)===1)return l;i=a||null}return t.join(" > ")}cssEscape(e){return window.CSS&&window.CSS.escape?window.CSS.escape(e):String(e).replace(/([ #.;?%&,.+*~':"!^$[\]()=>|/])/g,"\\$1")}safeQueryCount(e){try{return document.querySelectorAll(e).length}catch{return-1}}openModal(){this.state="review",this.elements.error.classList.remove("fbw-show"),this.elements.form.reset(),this.showItemInForm?(this.elements.itemField.classList.remove("fbw-hidden"),this.elements.itemBox.textContent=this.selectedSelector||"(none)"):this.elements.itemField.classList.add("fbw-hidden"),this.elements.overlay.classList.add("fbw-open"),setTimeout(()=>this.elements.feedback.focus(),30)}closeModal(){this.elements.overlay.classList.remove("fbw-open")}cancelAll(){this.stopPicking(),this.closeModal(),this.elements.highlight.style.display="none",document.removeEventListener("click",this.onDocumentClickCapture,!0),document.removeEventListener("keydown",this.onKeyDownCapture,!0),this.selectedEl=null,this.selectedSelector=null,this.state="idle"}onOverlayClick(e){e.target.closest('[data-action="close-modal"]')&&this.cancelAll()}onSubmit(e){e.preventDefault();let t=this.elements.title.value.trim(),i=this.elements.feedback.value.trim(),o=this.elements.email.value.trim(),n=/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(o);if(!t||!n){this.elements.error.textContent=t?"Please enter a valid email address.":"Please add a title before submitting.",this.elements.error.classList.add("fbw-show");return}let a={optionType:this.showItemInForm?"page_item_visible":"page_item_hidden",elementSelector:this.selectedSelector,pageUrl:window.location.href,pageTitle:document.title,title:t,feedback:i,email:o,submittedAt:new Date().toISOString()};this.submitFeedback(a)}prepareMessageBody(e){let t=e.email,i=e.title,o=e.feedback||"(No additional details provided)",n=e.pageTitle,a=`${o}
|
|
299
|
+
|
|
300
|
+
Page: ${n}
|
|
301
|
+
URL: ${e.pageUrl}
|
|
302
|
+
Selector: ${e.elementSelector}
|
|
303
|
+
Option: ${e.optionType}
|
|
304
|
+
Submitted: ${e.submittedAt}`;return{title:i,body:a,user_handle:t}}submitFeedback(e){if(window.dispatchEvent(new CustomEvent("feedbackwidget:submit",{detail:e})),this.apiKey){let t=this.prepareMessageBody(e);fetch(this.endPoint,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.apiKey}`},body:JSON.stringify(t)}).catch(()=>{})}this.cancelAll(),this.showToast("Thanks for the feedback!")}showToast(e){this.elements.toast.textContent=e,this.elements.toast.classList.add("fbw-show"),setTimeout(()=>this.elements.toast.classList.remove("fbw-show"),2400)}isInsideWidget(e){return e===this.host||this.host.contains(e)}open(e="visible"){this.startPicking(e==="visible")}close(){this.cancelAll()}destroy(){this.cancelAll(),this.host.remove()}};return g(w);})();
|
|
305
|
+
/*!
|
|
306
|
+
* Feedback Capture Widget - TypeScript Implementation
|
|
307
|
+
* Class-based, configurable widget with style isolation via Shadow DOM
|
|
308
|
+
* Supports multiple widget types (default, chatbot) and positioning (left, right)
|
|
309
|
+
*/
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Feedback Capture Widget - TypeScript Implementation
|
|
3
|
+
* Class-based, configurable widget with style isolation via Shadow DOM
|
|
4
|
+
* Supports multiple widget types (default, chatbot) and positioning (left, right)
|
|
5
|
+
*/
|
|
6
|
+
type WidgetPosition = 'left' | 'right';
|
|
7
|
+
type WidgetType = 'default' | 'chatbot';
|
|
8
|
+
interface FeedbackWidgetConfig {
|
|
9
|
+
buttonLabel?: string;
|
|
10
|
+
backgroundColor?: string;
|
|
11
|
+
textColor?: string;
|
|
12
|
+
accentColor?: string;
|
|
13
|
+
font?: string;
|
|
14
|
+
position?: WidgetPosition;
|
|
15
|
+
widgetType?: WidgetType;
|
|
16
|
+
}
|
|
17
|
+
interface FeedbackPayload {
|
|
18
|
+
optionType: 'page_item_visible' | 'page_item_hidden';
|
|
19
|
+
elementSelector: string | null;
|
|
20
|
+
pageUrl: string;
|
|
21
|
+
pageTitle: string;
|
|
22
|
+
title: string;
|
|
23
|
+
feedback: string;
|
|
24
|
+
email: string;
|
|
25
|
+
submittedAt: string;
|
|
26
|
+
}
|
|
27
|
+
declare class FeedbackWidget {
|
|
28
|
+
private config;
|
|
29
|
+
private host;
|
|
30
|
+
private root;
|
|
31
|
+
private elements;
|
|
32
|
+
private state;
|
|
33
|
+
private showItemInForm;
|
|
34
|
+
private selectedSelector;
|
|
35
|
+
private selectedEl;
|
|
36
|
+
private hoverRafId;
|
|
37
|
+
private hostId;
|
|
38
|
+
private apiKey;
|
|
39
|
+
private endPoint;
|
|
40
|
+
constructor(apiKey?: string | null, config?: FeedbackWidgetConfig);
|
|
41
|
+
private initialize;
|
|
42
|
+
private createHostAndShadow;
|
|
43
|
+
private injectStyles;
|
|
44
|
+
private generateCSS;
|
|
45
|
+
private hexToRgba;
|
|
46
|
+
private createMarkup;
|
|
47
|
+
private getMarkup;
|
|
48
|
+
private getDefaultMarkup;
|
|
49
|
+
private getChatbotMarkup;
|
|
50
|
+
private escapeHtml;
|
|
51
|
+
private cacheElements;
|
|
52
|
+
private attachEventListeners;
|
|
53
|
+
private onTabClick;
|
|
54
|
+
private openMenu;
|
|
55
|
+
private closeMenu;
|
|
56
|
+
private onOutsideMenuClick;
|
|
57
|
+
private onMenuClick;
|
|
58
|
+
private startPicking;
|
|
59
|
+
private stopPicking;
|
|
60
|
+
private onBannerClick;
|
|
61
|
+
private onHoverMove;
|
|
62
|
+
private positionHighlight;
|
|
63
|
+
private onScrollOrResize;
|
|
64
|
+
private onDocumentClickCapture;
|
|
65
|
+
private onKeyDownCapture;
|
|
66
|
+
private finalizeSelection;
|
|
67
|
+
private getUniqueSelector;
|
|
68
|
+
private cssEscape;
|
|
69
|
+
private safeQueryCount;
|
|
70
|
+
private openModal;
|
|
71
|
+
private closeModal;
|
|
72
|
+
private cancelAll;
|
|
73
|
+
private onOverlayClick;
|
|
74
|
+
private onSubmit;
|
|
75
|
+
private prepareMessageBody;
|
|
76
|
+
private submitFeedback;
|
|
77
|
+
private showToast;
|
|
78
|
+
private isInsideWidget;
|
|
79
|
+
open(mode?: 'visible' | 'hidden'): void;
|
|
80
|
+
close(): void;
|
|
81
|
+
destroy(): void;
|
|
82
|
+
}
|
|
83
|
+
export { FeedbackWidget };
|
|
84
|
+
export type { FeedbackWidgetConfig, FeedbackPayload, WidgetPosition, WidgetType };
|
|
85
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|