feedlet-widget
Version:
Lightweight JavaScript widget for capturing user feedback and exit-intent surveys
1,356 lines (1,273 loc) • 550 kB
JavaScript
var wa = Object.defineProperty;
var pa = (r, A, e) => A in r ? wa(r, A, { enumerable: !0, configurable: !0, writable: !0, value: e }) : r[A] = e;
var y = (r, A, e) => pa(r, typeof A != "symbol" ? A + "" : A, e);
class Qa {
constructor(A) {
y(this, "element", null);
y(this, "config");
y(this, "onFeedbackClick", null);
y(this, "onContextualClick", null);
y(this, "isContextualMode", !1);
this.config = A;
}
create(A, e) {
this.onFeedbackClick = A, this.onContextualClick = e || null;
const t = document.createElement("div");
return t.className = "feedlet-sticky-wrapper", this.element = document.createElement("div"), this.element.id = "feedlet-floating-button", this.element.className = "feedlet-floating-container", this.updateElement(), this.addStyles(), this.attachEventListeners(), t.appendChild(this.element), t;
}
updateElement() {
if (!this.element) return;
const A = this.config.enableContextualFeedback !== !1;
this.element.innerHTML = `
<div class="feedlet-social-bar">
${A ? `
<button
id="feedlet-contextual-fab"
class="feedlet-social-btn ${this.isContextualMode ? "active" : ""}"
title="Click to add contextual comments"
>
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clip-rule="evenodd"/>
</svg>
<span class="feedlet-social-label">Add Comment</span>
</button>
` : ""}
<button
id="feedlet-feedback-fab"
class="feedlet-social-btn"
title="Send feedback"
>
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M18 5v8a2 2 0 01-2 2h-5l-5 4v-4H4a2 2 0 01-2-2V5a2 2 0 012-2h12a2 2 0 012 2zM7 8H5v2h2V8zm2 0h2v2H9V8zm6 0h-2v2h2V8z" clip-rule="evenodd"/>
</svg>
<span class="feedlet-social-label">Feedback</span>
</button>
</div>
`;
}
addStyles() {
if (document.getElementById("feedlet-floating-styles")) return;
const A = document.createElement("style");
A.id = "feedlet-floating-styles", A.textContent = `
.feedlet-sticky-wrapper {
position: fixed;
top: 50%;
right: 0;
transform: translateY(-50%);
z-index: 999999;
pointer-events: none;
}
.feedlet-floating-container {
position: relative;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
pointer-events: auto;
}
.feedlet-social-bar {
display: flex;
flex-direction: column;
gap: 0;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
border-radius: 8px 0 0 8px;
overflow: hidden;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-right: none;
}
.feedlet-social-btn {
display: flex;
align-items: center;
justify-content: flex-start;
padding: 16px 12px;
border: none;
background: ${this.config.color || "#4f46e5"};
color: white;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
min-width: 50px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
gap: 8px;
}
.feedlet-social-btn:last-child {
border-bottom: none;
}
.feedlet-social-btn:hover {
background: ${this.adjustColor(this.config.color || "#4f46e5", 20)};
transform: translateX(-2px);
min-width: 120px;
}
.feedlet-social-btn.active {
background: ${this.adjustColor(this.config.color || "#4f46e5", 40)};
transform: translateX(-2px);
min-width: 120px;
}
.feedlet-social-btn:active {
transform: translateX(-1px) scale(0.98);
}
.feedlet-social-label {
font-size: 14px;
font-weight: 500;
opacity: 0;
transform: translateX(-10px);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
white-space: nowrap;
margin-left: 4px;
}
.feedlet-social-btn:hover .feedlet-social-label,
.feedlet-social-btn.active .feedlet-social-label {
opacity: 1;
transform: translateX(0);
}
/* Ripple effect */
.feedlet-social-btn::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.feedlet-social-btn:active::before {
width: 120px;
height: 120px;
}
/* Alternative color schemes for different buttons */
.feedlet-social-btn:nth-child(1) {
background: #1DA1F2; /* Twitter blue for comment */
}
.feedlet-social-btn:nth-child(1):hover {
background: #1a91da;
}
.feedlet-social-btn:nth-child(2) {
background: ${this.config.color || "#4f46e5"}; /* Keep config color for feedback */
}
.feedlet-social-btn:nth-child(2):hover {
background: ${this.adjustColor(this.config.color || "#4f46e5", 20)};
}
/* Mobile optimizations */
@media (max-width: 768px) {
.feedlet-social-btn {
padding: 14px 10px;
min-width: 44px;
}
.feedlet-social-btn:hover {
min-width: 100px;
}
.feedlet-social-label {
font-size: 12px;
}
}
/* Pulse animation for contextual mode */
.feedlet-social-btn.active {
animation: feedlet-pulse 2s infinite;
}
@keyframes feedlet-pulse {
0% { box-shadow: 0 0 0 0 rgba(29, 161, 242, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(29, 161, 242, 0); }
100% { box-shadow: 0 0 0 0 rgba(29, 161, 242, 0); }
}
`, document.head.appendChild(A);
}
adjustColor(A, e) {
const t = A[0] === "#", s = t ? A.slice(1) : A, n = parseInt(s, 16);
let i = (n >> 16) + e, a = (n >> 8 & 255) + e, o = (n & 255) + e;
return i = i > 255 ? 255 : i < 0 ? 0 : i, a = a > 255 ? 255 : a < 0 ? 0 : a, o = o > 255 ? 255 : o < 0 ? 0 : o, (t ? "#" : "") + (i << 16 | a << 8 | o).toString(16).padStart(6, "0");
}
attachEventListeners() {
if (!this.element) return;
const A = this.element.querySelector("#feedlet-contextual-fab");
A && this.onContextualClick && A.addEventListener("click", () => {
this.onContextualClick && this.onContextualClick();
});
const e = this.element.querySelector("#feedlet-feedback-fab");
e && this.onFeedbackClick && e.addEventListener("click", () => {
this.onFeedbackClick && this.onFeedbackClick();
});
}
show() {
this.element && (this.element.style.display = "block");
}
hide() {
this.element && (this.element.style.display = "none");
}
destroy() {
this.element && (this.element.remove(), this.element = null);
const A = document.getElementById("feedlet-floating-styles");
A && A.remove();
}
updateContextualMode(A) {
this.isContextualMode = A, this.updateElement(), this.attachEventListeners();
}
}
class Ca {
constructor(A) {
y(this, "config");
y(this, "element", null);
y(this, "isOpen", !1);
y(this, "eventListeners", /* @__PURE__ */ new Map());
y(this, "onClose", null);
y(this, "uploadedFiles", []);
y(this, "screenshots", []);
y(this, "currentThreadData", null);
y(this, "isDragging", !1);
y(this, "dragOffset", { x: 0, y: 0 });
y(this, "dragStartPosition", { x: 0, y: 0 });
this.config = A;
}
open(A, e) {
var s;
if (this.isOpen) return;
console.log("ExitBug: ThreadInterface.open called with data:", A), this.isOpen = !0, this.onClose = e || null, this.currentThreadData = A, this.createElement(A), this.addStyles(), this.attachEventListeners(), document.body.appendChild(this.element), this.positionElement(A.position);
const t = (s = this.element) == null ? void 0 : s.querySelector(".feedlet-thread-textarea");
t && t.focus(), requestAnimationFrame(() => {
var n;
(n = this.element) == null || n.classList.add("open");
}), this.emit("opened", { type: A.type });
}
close() {
this.isOpen && (this.isOpen = !1, this.uploadedFiles = [], this.screenshots = [], this.currentThreadData = null, this.element && (this.element.classList.remove("open"), setTimeout(() => {
this.element && (this.element.remove(), this.element = null);
}, 200)), this.onClose && this.onClose(), this.emit("closed", {}));
}
on(A, e) {
this.eventListeners.has(A) || this.eventListeners.set(A, []), this.eventListeners.get(A).push(e);
}
emit(A, e) {
const t = this.eventListeners.get(A);
t && t.forEach((s) => s(e));
}
isThreadOpen() {
return this.isOpen;
}
createElement(A) {
this.element = document.createElement("div"), this.element.className = "feedlet-thread-container", this.element.id = "feedlet-thread";
const e = A.type === "contextual", t = A.type === "comment", s = A.type === "exit-survey", n = e && A.marker, i = A.placeholder || (e || t ? "Add a comment..." : s ? "What could we have done better?" : "Start a new thread...");
this.element.innerHTML = `
<div class="feedlet-thread-box">
<div class="feedlet-thread-header">
<div class="feedlet-thread-title">
${n ? this.renderContextualTitle(A.marker) : s ? "🚪 Quick Survey" : e || t ? "Comment" : "Feedback"}
</div>
<button type="button" class="feedlet-thread-close-btn" title="Close">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
${s ? this.renderExitSurveyIntro() : ""}
<form class="feedlet-thread-form" id="feedlet-thread-form">
<div class="feedlet-thread-input-container">
<textarea
class="feedlet-thread-textarea"
placeholder="${i}"
rows="1"
required
></textarea>
<div class="feedlet-thread-attachments" id="feedlet-attachments"></div>
<div class="feedlet-thread-actions">
${s ? "" : `
<button type="button" class="feedlet-thread-action-btn" id="feedlet-file-upload" title="Add file">
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd"/>
</svg>
</button>
<button type="button" class="feedlet-thread-action-btn" id="feedlet-screenshot" title="Add screenshot (multiple allowed)">
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"></path>
</svg>
</button>
<button type="button" class="feedlet-thread-action-btn" title="Format text">
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1z"></path>
</svg>
</button>
`}
<button type="submit" class="feedlet-thread-submit-btn" title="${s ? "Submit survey" : "Send"}">
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
</svg>
</button>
</div>
</div>
${A.type === "feedback" ? this.renderFeedbackTypeSelector() : ""}
${s ? this.renderExitSurveyContact() : this.renderContactSection()}
</form>
<input type="file" id="feedlet-file-input" accept="image/*,.pdf,.doc,.docx,.txt" multiple style="display: none;">
</div>
<div class="feedlet-thread-backdrop" data-dismiss="true"></div>
`;
}
renderContextualTitle(A) {
return `
<div class="feedlet-thread-title-content">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>
<span class="feedlet-thread-title-text">${A.elementText ? `"${A.elementText}"` : A.elementSelector}</span>
</div>
`;
}
renderContextualInfo(A) {
return `
<div class="feedlet-thread-context">
<div class="feedlet-thread-context-pin">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
</svg>
</div>
<div class="feedlet-thread-context-text">
${A.elementText ? `"${A.elementText}"` : A.elementSelector}
</div>
</div>
`;
}
renderFeedbackTypeSelector() {
return `
<div class="feedlet-thread-type-selector">
<label class="feedlet-thread-type-option">
<input type="radio" name="type" value="bug" checked>
<span class="feedlet-thread-type-label">
<span class="feedlet-thread-type-icon">🐛</span>
Bug
</span>
</label>
<label class="feedlet-thread-type-option">
<input type="radio" name="type" value="idea">
<span class="feedlet-thread-type-label">
<span class="feedlet-thread-type-icon">💡</span>
Idea
</span>
</label>
<label class="feedlet-thread-type-option">
<input type="radio" name="type" value="other">
<span class="feedlet-thread-type-label">
<span class="feedlet-thread-type-icon">💬</span>
Other
</span>
</label>
</div>
`;
}
renderContactSection() {
return `
<div class="feedlet-thread-contact-section">
<details class="feedlet-thread-contact-details">
<summary class="feedlet-thread-contact-summary">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
</svg>
Get notified when this is resolved
</summary>
<div class="feedlet-thread-contact-content">
<p class="feedlet-thread-contact-description">
Want to know when we've addressed your feedback? Leave your email and we'll keep you updated.
</p>
<div class="feedlet-thread-contact-fields">
<input
type="email"
name="contact_email"
placeholder="your.email@example.com"
class="feedlet-thread-contact-email"
/>
<label class="feedlet-thread-contact-notify">
<input type="checkbox" name="notify_on_update" value="true" checked />
<span>Send me updates</span>
</label>
</div>
</div>
</details>
</div>
`;
}
renderExitSurveyIntro() {
return `
<div class="feedlet-thread-exit-intro">
<div class="feedlet-thread-exit-intro-content">
<p>Before you go, could you help us improve? Your feedback only takes a moment and helps us serve you better.</p>
</div>
</div>
`;
}
renderExitSurveyContact() {
return `
<div class="feedlet-thread-contact-section feedlet-thread-exit-contact">
<details class="feedlet-thread-contact-details">
<summary class="feedlet-thread-contact-summary">
<svg width="16" height="16" fill="currentColor" viewBox="0 0 20 20">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
</svg>
Stay in touch (optional)
</summary>
<div class="feedlet-thread-contact-content">
<p class="feedlet-thread-contact-description">
Want to hear about improvements based on your feedback? Leave your email.
</p>
<div class="feedlet-thread-contact-fields">
<input
type="email"
name="contact_email"
placeholder="your.email@example.com"
class="feedlet-thread-contact-email"
/>
<label class="feedlet-thread-contact-notify">
<input type="checkbox" name="notify_on_update" value="true" />
<span>Send me updates</span>
</label>
</div>
</div>
</details>
</div>
`;
}
positionElement(A) {
if (!this.element) return;
const e = {
width: window.innerWidth,
height: window.innerHeight
};
this.element.getBoundingClientRect();
const t = 360, s = 200;
let n = A.x, i = A.y;
n + t > e.width - 20 && (n = e.width - t - 20), n < 20 && (n = 20), i + s > e.height - 20 && (i = A.y - s - 10), i < 20 && (i = 20), this.element.style.left = `${n}px`, this.element.style.top = `${i}px`;
}
addStyles() {
if (document.getElementById("feedlet-thread-styles")) return;
const A = document.createElement("style");
A.id = "feedlet-thread-styles", A.textContent = `
.feedlet-thread-container {
position: fixed;
z-index: 1000000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
opacity: 0;
transform: scale(0.96) translateY(-8px);
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none;
}
.feedlet-thread-container.open {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.feedlet-thread-container.dragging {
transition: none;
}
.feedlet-thread-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: -1;
background: transparent;
}
.feedlet-thread-box {
width: 360px;
background: var(--feedlet-bg-primary, #1a1a1a);
border-radius: 12px;
box-shadow: 0 8px 16px var(--feedlet-shadow, rgba(0, 0, 0, 0.15));
border: 1px solid var(--feedlet-border, rgba(255, 255, 255, 0.12));
overflow: hidden;
backdrop-filter: var(--feedlet-backdrop-filter, blur(8px));
}
.feedlet-thread-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--feedlet-border, #333);
position: relative;
z-index: 10;
cursor: move;
user-select: none;
}
.feedlet-thread-header:hover {
background: var(--feedlet-border-light, rgba(255, 255, 255, 0.03));
}
.feedlet-thread-header.dragging {
cursor: grabbing;
background: var(--feedlet-border-light, rgba(255, 255, 255, 0.05));
}
.feedlet-thread-title {
color: var(--feedlet-text-primary, #ffffff);
font-size: 14px;
font-weight: 600;
flex: 1;
pointer-events: none;
}
.feedlet-thread-title-content {
display: flex;
align-items: center;
gap: 8px;
pointer-events: none;
}
.feedlet-thread-title-text {
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 12px;
background: var(--feedlet-bg-secondary, #333);
padding: 2px 6px;
border-radius: 4px;
color: var(--feedlet-text-secondary, #ccc);
max-width: 240px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
pointer-events: none;
}
.feedlet-thread-close-btn {
background: none;
border: none;
color: var(--feedlet-text-tertiary, #9CA3AF);
cursor: pointer;
padding: 4px;
border-radius: 6px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
flex-shrink: 0;
pointer-events: auto;
}
.feedlet-thread-close-btn:hover {
background: var(--feedlet-border-light, rgba(255, 255, 255, 0.1));
color: var(--feedlet-text-primary, #ffffff);
}
.feedlet-thread-context {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: var(--feedlet-bg-secondary, #2a2a2a);
border-bottom: 1px solid var(--feedlet-border, #333);
font-size: 13px;
color: var(--feedlet-text-tertiary, #999);
}
.feedlet-thread-context-pin {
color: ${this.config.color || "#4f46e5"};
display: flex;
align-items: center;
}
.feedlet-thread-context-text {
font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
font-size: 12px;
background: var(--feedlet-bg-secondary, #333);
padding: 2px 6px;
border-radius: 4px;
color: var(--feedlet-text-secondary, #ccc);
max-width: 280px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.feedlet-thread-form {
padding: 20px;
}
.feedlet-thread-input-container {
position: relative;
}
.feedlet-thread-textarea {
width: 100%;
background: transparent;
border: none;
outline: none;
resize: none;
font-size: 16px;
line-height: 1.5;
color: var(--feedlet-text-primary, #fff);
padding: 0 0 12px 0;
font-family: inherit;
min-height: 24px;
max-height: 120px;
}
.feedlet-thread-textarea::placeholder {
color: var(--feedlet-text-tertiary, #666);
}
.feedlet-thread-attachments {
margin: 8px 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.feedlet-thread-attachment {
display: flex;
align-items: center;
gap: 6px;
background: #2a2a2a;
border: 1px solid #333;
border-radius: 6px;
padding: 6px 8px;
font-size: 12px;
color: #ccc;
max-width: 200px;
}
.feedlet-thread-attachment-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.feedlet-thread-attachment-remove {
background: none;
border: none;
color: #666;
cursor: pointer;
padding: 2px;
border-radius: 3px;
transition: all 0.2s;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
}
.feedlet-thread-attachment-remove:hover {
background: #444;
color: #fff;
}
.feedlet-thread-attachment-preview {
width: 100%;
max-width: 200px;
height: 60px;
object-fit: cover;
border-radius: 4px;
background: #333;
display: flex;
align-items: center;
justify-content: center;
margin-top: 4px;
}
.feedlet-thread-exit-intro {
padding: 16px 20px;
background: var(--feedlet-bg-secondary, #2a2a2a);
border-bottom: 1px solid var(--feedlet-border, #333);
}
.feedlet-thread-exit-intro-content p {
margin: 0;
color: var(--feedlet-text-secondary, #d1d5db);
font-size: 14px;
line-height: 1.5;
text-align: center;
}
.feedlet-thread-actions {
display: flex;
align-items: center;
justify-content: space-between;
border-top: 1px solid var(--feedlet-border, #333);
padding-top: 12px;
}
.feedlet-thread-action-btn {
background: none;
border: none;
color: #999;
cursor: pointer;
padding: 8px;
border-radius: 6px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.feedlet-thread-action-btn:hover {
background: #333;
color: #fff;
}
.feedlet-thread-submit-btn {
background: ${this.config.color || "#4f46e5"};
border: none;
color: white;
cursor: pointer;
padding: 8px 12px;
border-radius: 8px;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
min-width: 44px;
}
.feedlet-thread-submit-btn:hover {
background: ${this.adjustColor(this.config.color || "#4f46e5", -20)};
transform: translateY(-1px);
}
.feedlet-thread-submit-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.feedlet-thread-type-selector {
display: flex;
gap: 8px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #333;
}
.feedlet-thread-type-option {
cursor: pointer;
flex: 1;
}
.feedlet-thread-type-option input[type="radio"] {
display: none;
}
.feedlet-thread-type-label {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
background: #2a2a2a;
border: 1px solid #333;
border-radius: 8px;
color: #999;
font-size: 13px;
font-weight: 500;
transition: all 0.2s ease;
justify-content: center;
}
.feedlet-thread-type-option:hover .feedlet-thread-type-label {
background: #333;
color: #fff;
}
.feedlet-thread-type-option input[type="radio"]:checked + .feedlet-thread-type-label {
background: ${this.config.color || "#4f46e5"}20;
border-color: ${this.config.color || "#4f46e5"};
color: ${this.config.color || "#4f46e5"};
}
.feedlet-thread-type-icon {
font-size: 14px;
}
@media (max-width: 640px) {
.feedlet-thread-box {
width: calc(100vw - 40px);
max-width: 360px;
}
}
.feedlet-thread-contact-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #333;
}
.feedlet-thread-contact-details {
border: none;
background: none;
}
.feedlet-thread-contact-summary {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #999;
cursor: pointer;
list-style: none;
padding: 0;
margin: 0;
transition: color 0.2s;
}
.feedlet-thread-contact-summary:hover {
color: #fff;
}
.feedlet-thread-contact-summary::-webkit-details-marker {
display: none;
}
.feedlet-thread-contact-content {
margin-top: 12px;
padding: 12px;
background: #2a2a2a;
border-radius: 8px;
border: 1px solid #333;
}
.feedlet-thread-contact-description {
margin: 0 0 12px 0;
font-size: 12px;
color: #ccc;
line-height: 1.5;
}
.feedlet-thread-contact-fields {
display: flex;
flex-direction: column;
gap: 8px;
}
.feedlet-thread-contact-email {
background: #1a1a1a;
border: 1px solid #444;
border-radius: 6px;
padding: 8px 12px;
color: #fff;
font-size: 13px;
transition: border-color 0.2s;
}
.feedlet-thread-contact-email:focus {
outline: none;
border-color: ${this.config.color || "#4f46e5"};
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.2);
}
.feedlet-thread-contact-email::placeholder {
color: #666;
}
.feedlet-thread-contact-notify {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #ccc;
cursor: pointer;
}
.feedlet-thread-contact-notify input[type="checkbox"] {
width: 16px;
height: 16px;
border: 1px solid #666;
border-radius: 3px;
background: transparent;
cursor: pointer;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
}
.feedlet-thread-contact-notify input[type="checkbox"]:checked {
background: ${this.config.color || "#4f46e5"};
border-color: ${this.config.color || "#4f46e5"};
}
.feedlet-thread-contact-notify input[type="checkbox"]:checked::after {
content: '✓';
color: white;
font-size: 11px;
font-weight: bold;
}
`, document.head.appendChild(A);
}
adjustColor(A, e) {
const t = A[0] === "#", s = t ? A.slice(1) : A, n = parseInt(s, 16);
let i = (n >> 16) + e, a = (n >> 8 & 255) + e, o = (n & 255) + e;
return i = i > 255 ? 255 : i < 0 ? 0 : i, a = a > 255 ? 255 : a < 0 ? 0 : a, o = o > 255 ? 255 : o < 0 ? 0 : o, (t ? "#" : "") + (i << 16 | a << 8 | o).toString(16).padStart(6, "0");
}
attachEventListeners() {
if (!this.element) return;
const A = this.element.querySelector(".feedlet-thread-close-btn");
A ? A.addEventListener("click", (l) => {
l.preventDefault(), l.stopPropagation(), l.stopImmediatePropagation(), console.log("ExitBug: Close button clicked"), this.close();
}, !0) : console.warn("ExitBug: Close button not found");
const e = this.element.querySelector(".feedlet-thread-backdrop");
e && e.addEventListener("click", () => {
this.close();
}), this.attachDragListeners();
const t = this.element.querySelector("#feedlet-file-upload"), s = this.element.querySelector("#feedlet-file-input");
t && s && (t.addEventListener("click", () => {
s.click();
}), s.addEventListener("change", (l) => {
const c = l.target;
c.files && c.files.length > 0 && this.handleFileUpload(c.files);
}));
const n = this.element.querySelector("#feedlet-screenshot");
n && n.addEventListener("click", () => {
this.handleScreenshotSelection();
});
const i = this.element.querySelector(".feedlet-thread-textarea");
i && (i.addEventListener("input", () => {
i.style.height = "auto", i.style.height = Math.min(i.scrollHeight, 120) + "px";
}), i.addEventListener("keydown", (l) => {
l.key === "Escape" && this.close();
})), document.addEventListener("keydown", (l) => {
l.key === "Escape" && this.isOpen && this.close();
});
const a = this.element.querySelector("#feedlet-thread-form");
a ? a.addEventListener("submit", (l) => {
console.log("ExitBug: Form submit event triggered"), l.preventDefault(), this.handleSubmit(a);
}) : console.warn("ExitBug: Form element not found");
const o = this.element.querySelector(".feedlet-thread-submit-btn");
o ? o.addEventListener("click", (l) => {
var u;
l.preventDefault(), l.stopPropagation(), l.stopImmediatePropagation(), console.log("ExitBug: Submit button clicked");
const c = (u = this.element) == null ? void 0 : u.querySelector("#feedlet-thread-form");
c && (console.log("ExitBug: Triggering form submission manually"), this.handleSubmit(c));
}, !0) : console.warn("ExitBug: Submit button not found");
}
attachDragListeners() {
if (!this.element) return;
const A = this.element.querySelector(".feedlet-thread-header");
if (!A) return;
const e = (n) => {
if (n.target.closest(".feedlet-thread-close-btn"))
return;
this.isDragging = !0, this.dragStartPosition = { x: n.clientX, y: n.clientY };
const i = this.element.getBoundingClientRect();
this.dragOffset = {
x: n.clientX - i.left,
y: n.clientY - i.top
}, A.classList.add("dragging"), this.element.classList.add("dragging"), document.addEventListener("mousemove", t), document.addEventListener("mouseup", s), n.preventDefault();
}, t = (n) => {
if (!this.isDragging || !this.element) return;
const i = n.clientX - this.dragOffset.x, a = n.clientY - this.dragOffset.y, o = {
width: window.innerWidth,
height: window.innerHeight
}, l = this.element.getBoundingClientRect(), c = l.width, u = l.height, h = Math.max(0, Math.min(i, o.width - c)), d = Math.max(0, Math.min(a, o.height - u));
this.element.style.left = `${h}px`, this.element.style.top = `${d}px`, n.preventDefault();
}, s = (n) => {
this.isDragging && (this.isDragging = !1, A.classList.remove("dragging"), this.element.classList.remove("dragging"), document.removeEventListener("mousemove", t), document.removeEventListener("mouseup", s), n.preventDefault());
};
A.addEventListener("mousedown", e);
}
handleFileUpload(A) {
var t;
const e = (t = this.element) == null ? void 0 : t.querySelector("#feedlet-attachments");
e && Array.from(A).forEach((s) => {
this.uploadedFiles.push(s), this.addFileToDisplay(s, e);
});
}
addFileToDisplay(A, e) {
const t = document.createElement("div");
t.className = "feedlet-thread-attachment", t.dataset.fileName = A.name;
const s = A.type.startsWith("image/"), n = s ? "🖼️" : "📎";
t.innerHTML = `
${s ? '<div class="feedlet-thread-attachment-preview" id="preview-' + Date.now() + '"></div>' : ""}
<div style="display: flex; align-items: center; gap: 6px; width: 100%;">
<span>${n}</span>
<div class="feedlet-thread-attachment-name" title="${A.name}">${A.name}</div>
<button type="button" class="feedlet-thread-attachment-remove" title="Remove">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
`;
const i = t.querySelector(".feedlet-thread-attachment-remove");
if (i && i.addEventListener("click", () => {
this.removeFile(A.name), t.remove();
}), s) {
const a = new FileReader();
a.onload = (o) => {
var c;
const l = t.querySelector(`#preview-${Date.now()}`);
l && ((c = o.target) != null && c.result) && (l.innerHTML = `<img src="${o.target.result}" style="width: 100%; height: 60px; object-fit: cover; border-radius: 4px;" alt="Preview">`);
}, a.readAsDataURL(A);
}
e.appendChild(t);
}
removeFile(A) {
this.uploadedFiles = this.uploadedFiles.filter((e) => e.name !== A);
}
addScreenshotToDisplay(A, e, t) {
const s = document.createElement("div");
s.className = "feedlet-thread-attachment", s.dataset.fileName = A.name, s.innerHTML = `
<div class="feedlet-thread-attachment-preview" id="preview-${Date.now()}"></div>
<div style="display: flex; align-items: center; gap: 6px; width: 100%;">
<span>📷</span>
<div class="feedlet-thread-attachment-name" title="${A.name}">${A.name}</div>
<button type="button" class="feedlet-thread-attachment-remove" title="Remove">
<svg width="12" height="12" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
</svg>
</button>
</div>
`;
const n = s.querySelector(".feedlet-thread-attachment-remove");
n && n.addEventListener("click", () => {
this.removeScreenshot(A.name), s.remove();
});
const i = new FileReader();
i.onload = (a) => {
var l;
const o = s.querySelector(`#preview-${Date.now()}`);
o && ((l = a.target) != null && l.result) && (o.innerHTML = `<img src="${a.target.result}" style="width: 100%; height: 60px; object-fit: cover; border-radius: 4px;" alt="Screenshot preview">`);
}, i.readAsDataURL(e), t.appendChild(s);
}
removeScreenshot(A) {
const e = this.screenshots.findIndex((t, s) => `screenshot-${Date.now()}.png` === A);
e > -1 && this.screenshots.splice(e, 1);
}
async handleScreenshotSelection() {
const A = document.createElement("div");
A.className = "feedlet-screenshot-overlay", A.innerHTML = `
<div class="feedlet-screenshot-instructions">
<p>Click and drag to select an area to screenshot</p>
<p>You can take multiple screenshots by clicking the button again</p>
<p>Press ESC to cancel</p>
</div>
`;
const e = document.createElement("style");
e.textContent = `
.feedlet-screenshot-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.3);
z-index: 2000000;
cursor: crosshair;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.feedlet-screenshot-instructions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #1a1a1a;
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
pointer-events: none;
}
.feedlet-screenshot-instructions p {
margin: 8px 0;
}
.feedlet-screenshot-selection {
position: absolute;
border: 2px dashed #4f46e5;
background: rgba(79, 70, 229, 0.1);
pointer-events: none;
}
`, document.head.appendChild(e), document.body.appendChild(A);
let t = !1, s = 0, n = 0, i = null;
const a = (h) => {
h.target === A && (t = !0, s = h.clientX, n = h.clientY, i = document.createElement("div"), i.className = "feedlet-screenshot-selection", i.style.left = s + "px", i.style.top = n + "px", A.appendChild(i));
}, o = (h) => {
if (!t || !i) return;
const d = h.clientX, B = h.clientY, f = Math.min(s, d), w = Math.min(n, B), g = Math.abs(d - s), Q = Math.abs(B - n);
i.style.left = f + "px", i.style.top = w + "px", i.style.width = g + "px", i.style.height = Q + "px";
}, l = async (h) => {
var B;
if (!t || !i) return;
const d = i.getBoundingClientRect();
if (d.width > 10 && d.height > 10) {
A.style.display = "none";
try {
const f = (await Promise.resolve().then(() => Xh)).default;
(await f(document.body, {
x: d.left,
y: d.top,
width: d.width,
height: d.height,
useCORS: !0,
allowTaint: !0,
scale: 1,
backgroundColor: null
})).toBlob((g) => {
var Q;
if (g) {
const C = new File([g], `screenshot-${Date.now()}.png`, { type: "image/png" });
this.screenshots.push(g);
const v = (Q = this.element) == null ? void 0 : Q.querySelector("#feedlet-attachments");
v && this.addScreenshotToDisplay(C, g, v);
}
}, "image/png");
} catch (f) {
console.error("Screenshot capture failed:", f);
const w = (B = this.element) == null ? void 0 : B.querySelector(".feedlet-thread-textarea");
if (w) {
const g = w.value, Q = g + (g ? `
` : "") + `📷 Screenshot area selected (${Math.round(d.width)}×${Math.round(d.height)})`;
w.value = Q, w.dispatchEvent(new Event("input")), w.focus();
}
}
}
u();
}, c = (h) => {
h.key === "Escape" && u();
}, u = () => {
t = !1, A.remove(), e.remove(), document.removeEventListener("mousedown", a), document.removeEventListener("mousemove", o), document.removeEventListener("mouseup", l), document.removeEventListener("keydown", c);
};
document.addEventListener("mousedown", a), document.addEventListener("mousemove", o), document.addEventListener("mouseup", l), document.addEventListener("keydown", c), setTimeout(() => {
document.body.contains(A) && u();
}, 3e4);
}
handleSubmit(A) {
console.log("ExitBug: handleSubmit called");
const e = new FormData(A), t = A.querySelector(".feedlet-thread-textarea"), s = {
note: t.value.trim(),
files: this.uploadedFiles,
screenshots: this.screenshots
};
for (const [i, a] of e.entries())
s[i] = a;
if (this.currentThreadData && (this.currentThreadData.marker && (s.marker = this.currentThreadData.marker, s.isContextual = !0), this.currentThreadData.viewportInfo && (s.viewportInfo = this.currentThreadData.viewportInfo), s.threadType = this.currentThreadData.type), console.log("ExitBug: Form data with contextual info:", s), !s.note && this.uploadedFiles.length === 0 && this.screenshots.length === 0) {
console.log("ExitBug: No content to submit - note:", s.note, "files:", this.uploadedFiles.length, "screenshots:", this.screenshots.length), t.focus(), t.style.borderColor = "#ef4444", setTimeout(() => {
t.style.borderColor = "transparent";
}, 2e3);
return;
}
const n = A.querySelector(".feedlet-thread-submit-btn");
n && (n.disabled = !0, n.innerHTML = `
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path d="M10 2a8 8 0 100 16 8 8 0 000-16zm0 3a1 1 0 011 1v4a1 1 0 11-2 0V6a1 1 0 011-1zm0 7a1 1 0 100 2 1 1 0 000-2z"></path>
</svg>
`, setTimeout(() => {
n.disabled = !1, n.innerHTML = `
<svg width="18" height="18" fill="currentColor" viewBox="0 0 20 20">
<path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z"></path>
</svg>
`;
}, 2e3)), this.emit("submit", s), setTimeout(() => {
var a;
t.value = "", t.style.height = "auto", this.uploadedFiles = [], this.screenshots = [];
const i = (a = this.element) == null ? void 0 : a.querySelector("#feedlet-attachments");
i && (i.innerHTML = ""), t.focus();
}, 100);
}
destroy() {
this.close();
const A = document.getElementById("feedlet-thread-styles");
A && A.remove();
}
}
class Ua {
constructor(A) {
y(this, "isActive", !1);
y(this, "sensitivity");
y(this, "delay");
y(this, "onExitIntent");
y(this, "inactivityTimer", null);
y(this, "delayTimer", null);
y(this, "lastActivity", Date.now());
y(this, "exitTriggered", !1);
y(this, "sessionStartTime", Date.now());
this.sensitivity = A.sensitivity, this.delay = A.delay || 1e3, this.onExitIntent = A.onExitIntent;
}
start() {
this.isActive || (this.isActive = !0, this.exitTriggered = !1, this.addEventListeners());
}
stop() {
this.isActive = !1, this.removeEventListeners(), this.clearTimers();
}
addEventListeners() {
document.addEventListener("mousemove", this.handleMouseMove.bind(this)), document.addEventListener("mouseout", this.handleMouseOut.bind(this)), document.addEventListener("visibilitychange", this.handleVisibilityChange.bind(this)), this.isMobile() && (document.addEventListener("touchstart", this.handleTouchStart.bind(this)), window.addEventListener("beforeunload", this.handleBeforeUnload.bind(this)));
}
removeEventListeners() {
document.removeEventListener("mousemove", this.handleMouseMove.bind(this)), document.removeEventListener("mouseout", this.handleMouseOut.bind(this)), document.removeEventListener("visibilitychange", this.handleVisibilityChange.bind(this)), this.isMobile() && (document.removeEventListener("touchstart", this.handleTouchStart.bind(this)), window.removeEventListener("beforeunload", this.handleBeforeUnload.bind(this)));
}
handleMouseMove(A) {
this.lastActivity = Date.now(), this.resetInactivityTimer();
}
handleMouseOut(A) {
A.clientY <= this.sensitivity && this.lastActivity > this.lastActivity && this.triggerExitIntent();
}
handleVisibilityChange() {
document.hidden && this.triggerExitIntent();
}
handleTouchStart() {
this.lastActivity = Date.now(), this.resetInactivityTimer();
}
handleBeforeUnload() {
this.triggerExitIntent();
}
resetInactivityTimer() {
this.inactivityTimer && clearTimeout(this.inactivityTimer), this.inactivityTimer = setTimeout(() => {
this.triggerExitIntent();
}, 3e4);
}
triggerExitIntent() {
this.exitTriggered || !this.isActive || Date.now() - this.sessionStartTime < this.delay || (this.exitTriggered = !0, this.delayTimer && clearTimeout(this.delayTimer), this.delayTimer = setTimeout(() => {
this.onExitIntent();
}, 100));
}
clearTimers() {
this.delayTimer && (clearTimeout(this.delayTimer), this.delayTimer = null), this.inactivityTimer && (clearTimeout(this.inactivityTimer), this.inactivityTimer = null);
}
isMobile() {
return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
}
reset() {
this.exitTriggered = !1, this.sessionStartTime = Date.now();
}
isTriggered() {
return this.exitTriggered;
}
getTimeOnPage() {
return Date.now() - this.sessionStartTime;
}
}
/*!
* html2canvas 1.4.1 <https://html2canvas.hertzen.com>
* Copyright (c) 2022 Niklas von Hertzen <https://hertzen.com>
* Released under MIT License
*/
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
var Kr = function(r, A) {
return Kr = Object.setPrototypeOf || { __proto__: [] } instanceof Array && function(e, t) {
e.__proto__ = t;
} || function(e, t) {
for (var s in t) Object.prototype.hasOwnProperty.call(t, s) && (e[s] = t[s]);
}, Kr(r, A);
};
function BA(r, A) {
if (typeof A != "function" && A !== null)
throw new TypeError("Class extends value " + String(A) + " is not a constructor or null");
Kr(r, A);
function e() {
this.constructor = r;
}
r.prototype = A === null ? Object.create(A) : (e.prototype = A.prototype, new e());
}
var Dr = function() {
return Dr = Object.assign || function(A) {
for (var e, t = 1, s = arguments.length; t < s; t++) {
e = arguments[t];
for (var n in e) Object.prototype.hasOwnProperty.call(e, n) && (A[n] = e[n]);
}
return A;
}, Dr.apply(this, arguments);
};
function q(r, A, e, t) {
function s(n) {
return n instanceof e ? n : new e(function(i) {
i(n);
});
}
return new (e || (e = Promise))(function(n, i) {
function a(c) {
try {
l(t.next(c));
} catch (u) {
i(u);
}
}
function o(c) {
try {
l(t.throw(c));
} catch (u) {
i(u);
}
}
function l(c) {
c.done ? n(c.value) : s(c.value).then(a, o);
}
l((t = t.apply(r, [])).next());
});
}
function W(r, A) {
var e = { label: 0, sent: function() {
if (n[0] & 1) throw n[1];
return n[1];
}, trys: [], ops: [] }, t, s, n, i;
return i = { next: a(0), throw: a(1), return: a(2) }, typeof Symbol == "function" && (i[Symbol.iterator] = function() {
return this;
}), i;
function a(l) {
return function(c) {
return o([l, c]);
};
}
function o(l) {
if (t) throw new TypeError("Generator is already executing.");
for (; e; ) try {
if (t = 1, s && (n = l[0] & 2 ? s.return : l[0] ? s.throw || ((n = s.return) && n.call(s), 0) : s.next) && !(n = n.call(s, l[1])).done) re