feedbackkit-web 1.0.48 → 1.0.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/feedbackkit.iife.js +10 -10
- package/dist/feedbackkit.iife.js.map +1 -1
- package/dist/feedbackkit.js +124 -34
- package/dist/feedbackkit.js.map +1 -1
- package/dist/types/global.d.ts +1 -0
- package/dist/types/index.d.ts +16 -0
- package/dist/types/types.d.ts +20 -2
- package/dist/types/ui/widget.d.ts +19 -0
- package/package.json +1 -1
package/dist/feedbackkit.js
CHANGED
|
@@ -1672,7 +1672,11 @@ class Widget {
|
|
|
1672
1672
|
button2.dataset.position = options.position ?? "bottom-right";
|
|
1673
1673
|
button2.dataset.compact = String(!!options.compact);
|
|
1674
1674
|
const label = options.label ?? "Feedback";
|
|
1675
|
-
|
|
1675
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform || navigator.userAgent);
|
|
1676
|
+
const shortcutLabel = isMac ? "⌘⇧F" : "Ctrl+Shift+F";
|
|
1677
|
+
button2.title = `${label} (${shortcutLabel})`;
|
|
1678
|
+
button2.setAttribute("aria-label", `${label} (${shortcutLabel})`);
|
|
1679
|
+
button2.setAttribute("aria-keyshortcuts", isMac ? "Meta+Shift+F" : "Control+Shift+F");
|
|
1676
1680
|
button2.innerHTML = `${icons.feedback}<span class="fk-trigger-label"></span>`;
|
|
1677
1681
|
button2.querySelector(".fk-trigger-label").textContent = label;
|
|
1678
1682
|
button2.addEventListener("click", onClick);
|
|
@@ -1725,16 +1729,38 @@ class Widget {
|
|
|
1725
1729
|
if (secondary) host.style.setProperty("--fk-secondary", secondary);
|
|
1726
1730
|
else host.style.removeProperty("--fk-secondary");
|
|
1727
1731
|
}
|
|
1732
|
+
shouldCaptureScreenshot(options, captureOptions) {
|
|
1733
|
+
if (options.captureScreenshot !== void 0) return options.captureScreenshot;
|
|
1734
|
+
if (options.includeScreenshot !== void 0) return options.includeScreenshot;
|
|
1735
|
+
if (captureOptions.enabled !== void 0) return captureOptions.enabled;
|
|
1736
|
+
if (captureOptions.includeScreenshot !== void 0) return captureOptions.includeScreenshot;
|
|
1737
|
+
if (captureOptions.mode === "off") return false;
|
|
1738
|
+
if (this.deps.includeScreenshot && !this.deps.includeScreenshot()) return false;
|
|
1739
|
+
const config = this.deps.configuration();
|
|
1740
|
+
if (config) {
|
|
1741
|
+
if (typeof config.captureScreenshot === "boolean") return config.captureScreenshot;
|
|
1742
|
+
if (typeof config.captureScreenshot === "object" && config.captureScreenshot !== null) {
|
|
1743
|
+
if (config.captureScreenshot.enabled !== void 0) return config.captureScreenshot.enabled;
|
|
1744
|
+
if (config.captureScreenshot.includeScreenshot !== void 0) return config.captureScreenshot.includeScreenshot;
|
|
1745
|
+
if (config.captureScreenshot.mode === "off") return false;
|
|
1746
|
+
}
|
|
1747
|
+
if (config.includeScreenshot !== void 0) return config.includeScreenshot;
|
|
1748
|
+
}
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1728
1751
|
async run(options) {
|
|
1729
1752
|
const shadow = this.mount();
|
|
1730
1753
|
this.applyTheme();
|
|
1731
|
-
this.trigger?.setAttribute("aria-busy", "true");
|
|
1732
1754
|
const captureOptions = this.deps.captureOptions();
|
|
1755
|
+
const shouldCapture = this.shouldCaptureScreenshot(options, captureOptions);
|
|
1756
|
+
if (shouldCapture) {
|
|
1757
|
+
this.trigger?.setAttribute("aria-busy", "true");
|
|
1758
|
+
}
|
|
1733
1759
|
const [shot, environment] = await Promise.all([
|
|
1734
|
-
captureViewport({ ...captureOptions, exclude: this.isOwnNode }).catch((error) => {
|
|
1760
|
+
shouldCapture ? captureViewport({ ...captureOptions, exclude: this.isOwnNode }).catch((error) => {
|
|
1735
1761
|
console.warn("[FeedbackKit] Screenshot capture failed; continuing without one.", error);
|
|
1736
1762
|
return null;
|
|
1737
|
-
}),
|
|
1763
|
+
}) : Promise.resolve(null),
|
|
1738
1764
|
collectEnvironment({
|
|
1739
1765
|
screenName: this.deps.currentScreen(),
|
|
1740
1766
|
appVersion: this.deps.configuration()?.appVersion,
|
|
@@ -1744,21 +1770,31 @@ class Widget {
|
|
|
1744
1770
|
const logs = this.deps.logs();
|
|
1745
1771
|
this.trigger?.removeAttribute("aria-busy");
|
|
1746
1772
|
return new Promise((resolve) => {
|
|
1747
|
-
const dialog = new Dialog(
|
|
1748
|
-
|
|
1749
|
-
|
|
1773
|
+
const dialog = new Dialog(
|
|
1774
|
+
shadow,
|
|
1775
|
+
shot,
|
|
1776
|
+
environment,
|
|
1777
|
+
logs,
|
|
1778
|
+
this.deps,
|
|
1779
|
+
options,
|
|
1780
|
+
!shouldCapture,
|
|
1781
|
+
(result) => {
|
|
1782
|
+
resolve(result);
|
|
1783
|
+
}
|
|
1784
|
+
);
|
|
1750
1785
|
dialog.show();
|
|
1751
1786
|
});
|
|
1752
1787
|
}
|
|
1753
1788
|
}
|
|
1754
1789
|
class Dialog {
|
|
1755
|
-
constructor(shadow, shot, environment, logs, deps, options, done) {
|
|
1790
|
+
constructor(shadow, shot, environment, logs, deps, options, screenshotDisabled, done) {
|
|
1756
1791
|
this.shadow = shadow;
|
|
1757
1792
|
this.shot = shot;
|
|
1758
1793
|
this.environment = environment;
|
|
1759
1794
|
this.logs = logs;
|
|
1760
1795
|
this.deps = deps;
|
|
1761
1796
|
this.options = options;
|
|
1797
|
+
this.screenshotDisabled = screenshotDisabled;
|
|
1762
1798
|
this.done = done;
|
|
1763
1799
|
this.editor = null;
|
|
1764
1800
|
this.includeLogs = true;
|
|
@@ -1779,7 +1815,13 @@ class Dialog {
|
|
|
1779
1815
|
this.toolButtons = /* @__PURE__ */ new Map();
|
|
1780
1816
|
this.swatches = [];
|
|
1781
1817
|
this.onKeyDown = (e) => this.handleKey(e);
|
|
1782
|
-
this.
|
|
1818
|
+
if (this.screenshotDisabled || !shot) {
|
|
1819
|
+
this.includeScreenshot = false;
|
|
1820
|
+
} else if (options.includeScreenshot !== void 0) {
|
|
1821
|
+
this.includeScreenshot = options.includeScreenshot;
|
|
1822
|
+
} else {
|
|
1823
|
+
this.includeScreenshot = true;
|
|
1824
|
+
}
|
|
1783
1825
|
}
|
|
1784
1826
|
show() {
|
|
1785
1827
|
this.previousFocus = document.activeElement;
|
|
@@ -1853,7 +1895,7 @@ class Dialog {
|
|
|
1853
1895
|
this.resizeObserver.observe(wrap);
|
|
1854
1896
|
}
|
|
1855
1897
|
const note = el("div", "fk-off-note");
|
|
1856
|
-
note.textContent = this.shot ? "Screenshot won't be included" : "Screenshot unavailable on this page";
|
|
1898
|
+
note.textContent = this.shot || this.screenshotDisabled ? "Screenshot won't be included" : "Screenshot unavailable on this page";
|
|
1857
1899
|
wrap.append(note);
|
|
1858
1900
|
stage.append(toolbar, wrap);
|
|
1859
1901
|
return stage;
|
|
@@ -1883,6 +1925,16 @@ class Dialog {
|
|
|
1883
1925
|
const close = iconButton("fk-close", icons.close, "Close");
|
|
1884
1926
|
close.addEventListener("click", () => this.cancel());
|
|
1885
1927
|
header.append(title, close);
|
|
1928
|
+
let renderAttachment = () => {
|
|
1929
|
+
};
|
|
1930
|
+
const attachFile = (file) => {
|
|
1931
|
+
if (file.size > MAX_ATTACHMENT_BYTES) {
|
|
1932
|
+
this.showError(`"${file.name}" is larger than 10 MB.`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
this.attachment = { filename: file.name, mimeType: file.type || "application/octet-stream", data: file };
|
|
1936
|
+
renderAttachment();
|
|
1937
|
+
};
|
|
1886
1938
|
const textLabel = el("label", "fk-label");
|
|
1887
1939
|
textLabel.textContent = this.options.copy?.label ?? "What's the problem?";
|
|
1888
1940
|
textLabel.setAttribute("for", "fk-text");
|
|
@@ -1891,6 +1943,27 @@ class Dialog {
|
|
|
1891
1943
|
this.textarea.className = "fk-textarea";
|
|
1892
1944
|
this.textarea.placeholder = this.options.copy?.placeholder ?? "Describe what happened and what you expected instead…";
|
|
1893
1945
|
this.textarea.addEventListener("input", () => this.refreshState());
|
|
1946
|
+
this.textarea.addEventListener("paste", (e) => {
|
|
1947
|
+
const items = e.clipboardData?.items;
|
|
1948
|
+
if (!items || items.length === 0) return;
|
|
1949
|
+
for (let i = 0; i < items.length; i++) {
|
|
1950
|
+
const item = items[i];
|
|
1951
|
+
if (item.kind === "file") {
|
|
1952
|
+
const file = item.getAsFile();
|
|
1953
|
+
if (file) {
|
|
1954
|
+
let filename = file.name;
|
|
1955
|
+
if (!filename || filename === "image.png") {
|
|
1956
|
+
const ext = file.type.startsWith("image/") ? file.type.split("/")[1] || "png" : "bin";
|
|
1957
|
+
filename = `clipboard-${Date.now()}.${ext}`;
|
|
1958
|
+
}
|
|
1959
|
+
const attached = new File([file], filename, { type: file.type || "application/octet-stream" });
|
|
1960
|
+
attachFile(attached);
|
|
1961
|
+
e.preventDefault();
|
|
1962
|
+
break;
|
|
1963
|
+
}
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1894
1967
|
const textGroup = el("div");
|
|
1895
1968
|
textGroup.append(textLabel, this.textarea);
|
|
1896
1969
|
const productsGroup = el("div");
|
|
@@ -1909,12 +1982,7 @@ class Dialog {
|
|
|
1909
1982
|
const file = input.files?.[0];
|
|
1910
1983
|
input.value = "";
|
|
1911
1984
|
if (!file) return;
|
|
1912
|
-
|
|
1913
|
-
this.showError(`"${file.name}" is larger than 10 MB.`);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
this.attachment = { filename: file.name, mimeType: file.type || "application/octet-stream", data: file };
|
|
1917
|
-
renderAttachment();
|
|
1985
|
+
attachFile(file);
|
|
1918
1986
|
};
|
|
1919
1987
|
fileInput.addEventListener("change", () => onFile(fileInput));
|
|
1920
1988
|
cameraInput.addEventListener("change", () => onFile(cameraInput));
|
|
@@ -1926,7 +1994,7 @@ class Dialog {
|
|
|
1926
1994
|
const summary = el("span", "fk-options-summary");
|
|
1927
1995
|
const renderSummary = () => {
|
|
1928
1996
|
const parts = [];
|
|
1929
|
-
if (this.shot
|
|
1997
|
+
if (!this.shot) parts.push("No screenshot");
|
|
1930
1998
|
if (this.notifyReporter) parts.push("Notify me");
|
|
1931
1999
|
summary.textContent = parts.join(" · ");
|
|
1932
2000
|
};
|
|
@@ -1950,16 +2018,6 @@ class Dialog {
|
|
|
1950
2018
|
menu.append(item("Attach file…", () => fileInput.click()));
|
|
1951
2019
|
if (window.matchMedia?.("(pointer: coarse)").matches) menu.append(item("Take photo", () => cameraInput.click()));
|
|
1952
2020
|
menu.append(el("hr", "fk-menu-sep"));
|
|
1953
|
-
if (this.shot) {
|
|
1954
|
-
menu.append(
|
|
1955
|
-
item("Include screenshot", () => {
|
|
1956
|
-
this.includeScreenshot = !this.includeScreenshot;
|
|
1957
|
-
this.stage.dataset.screenshot = this.includeScreenshot ? "on" : "off";
|
|
1958
|
-
renderSummary();
|
|
1959
|
-
this.refreshState();
|
|
1960
|
-
}, this.includeScreenshot)
|
|
1961
|
-
);
|
|
1962
|
-
}
|
|
1963
2021
|
menu.append(
|
|
1964
2022
|
item("Notify me when it's fixed", () => {
|
|
1965
2023
|
this.notifyReporter = !this.notifyReporter;
|
|
@@ -1973,7 +2031,7 @@ class Dialog {
|
|
|
1973
2031
|
menu.querySelector(".fk-menu-item")?.focus();
|
|
1974
2032
|
};
|
|
1975
2033
|
const attachGroup = el("div", "fk-attach-group");
|
|
1976
|
-
|
|
2034
|
+
renderAttachment = () => {
|
|
1977
2035
|
attachGroup.replaceChildren();
|
|
1978
2036
|
if (!this.attachment) return;
|
|
1979
2037
|
const chip = el("span", "fk-file");
|
|
@@ -2010,7 +2068,17 @@ class Dialog {
|
|
|
2010
2068
|
this.sendButton.title = "Send (⌘/Ctrl + Enter)";
|
|
2011
2069
|
this.sendButton.addEventListener("click", () => void this.send());
|
|
2012
2070
|
actions.append(cancel, this.sendButton);
|
|
2013
|
-
composer.append(header, textGroup, productsGroup
|
|
2071
|
+
composer.append(header, textGroup, productsGroup);
|
|
2072
|
+
if (this.shot) {
|
|
2073
|
+
composer.append(
|
|
2074
|
+
switchControl("Include screenshot", this.includeScreenshot, (on) => {
|
|
2075
|
+
this.includeScreenshot = on;
|
|
2076
|
+
this.stage.dataset.screenshot = on ? "on" : "off";
|
|
2077
|
+
this.refreshState();
|
|
2078
|
+
})
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
composer.append(optionsRow);
|
|
2014
2082
|
if (meta) composer.append(meta);
|
|
2015
2083
|
composer.append(el("div", "fk-spacer"), this.errorBox, actions);
|
|
2016
2084
|
this.refreshState();
|
|
@@ -2281,6 +2349,7 @@ const widget = new Widget({
|
|
|
2281
2349
|
defaultProductKey: () => configuration?.defaultProductKey,
|
|
2282
2350
|
logs: () => logCapture?.buffer.snapshot() ?? [],
|
|
2283
2351
|
captureOptions: () => FeedbackKit.captureOptions,
|
|
2352
|
+
includeScreenshot: () => FeedbackKit.includeScreenshot,
|
|
2284
2353
|
currentScreen: () => FeedbackKit.currentScreen,
|
|
2285
2354
|
identity: () => ({ reporterId: reporterId(), user: getUser() })
|
|
2286
2355
|
});
|
|
@@ -2369,6 +2438,8 @@ const FeedbackKit = {
|
|
|
2369
2438
|
theme: null,
|
|
2370
2439
|
/** Screenshot capture settings. See `CaptureOptions`. */
|
|
2371
2440
|
captureOptions: {},
|
|
2441
|
+
/** Whether screenshots should be captured and included in reports by default. Default `true`. */
|
|
2442
|
+
includeScreenshot: true,
|
|
2372
2443
|
/** The hosted dashboard's ingestion endpoint, used when `configure` gets no `endpoint`. */
|
|
2373
2444
|
DEFAULT_ENDPOINT,
|
|
2374
2445
|
/**
|
|
@@ -2401,7 +2472,12 @@ const FeedbackKit = {
|
|
|
2401
2472
|
* `null` if the user cancels. Nothing is sent anywhere; delivery is yours.
|
|
2402
2473
|
*/
|
|
2403
2474
|
present(options = {}) {
|
|
2404
|
-
return widget.open({
|
|
2475
|
+
return widget.open({
|
|
2476
|
+
submit: false,
|
|
2477
|
+
onReport: options.onReport,
|
|
2478
|
+
captureScreenshot: options.captureScreenshot,
|
|
2479
|
+
includeScreenshot: options.includeScreenshot
|
|
2480
|
+
});
|
|
2405
2481
|
},
|
|
2406
2482
|
/**
|
|
2407
2483
|
* Opens the dialog and, on Send, delivers the report to the configured
|
|
@@ -2412,7 +2488,13 @@ const FeedbackKit = {
|
|
|
2412
2488
|
if (!configuration) {
|
|
2413
2489
|
return Promise.reject(new Error("FeedbackKit.configure({ projectKey }) must be called before presentAndSubmit()."));
|
|
2414
2490
|
}
|
|
2415
|
-
return widget.open({
|
|
2491
|
+
return widget.open({
|
|
2492
|
+
submit: true,
|
|
2493
|
+
onReport: options.onSubmitted,
|
|
2494
|
+
onError: options.onError,
|
|
2495
|
+
captureScreenshot: options.captureScreenshot,
|
|
2496
|
+
includeScreenshot: options.includeScreenshot
|
|
2497
|
+
});
|
|
2416
2498
|
},
|
|
2417
2499
|
/** Sends an already-built report (e.g. one from `present`) to the configured endpoint. */
|
|
2418
2500
|
submit(report) {
|
|
@@ -2429,10 +2511,17 @@ const FeedbackKit = {
|
|
|
2429
2511
|
* configured, otherwise `present` and logs the report to the console.
|
|
2430
2512
|
*/
|
|
2431
2513
|
showFloatingTriggerButton(options = {}) {
|
|
2514
|
+
if (options.keyboardShortcut !== false && options.enableKeyboardShortcut !== false && !shortcutHandler) {
|
|
2515
|
+
FeedbackKit.enableKeyboardShortcut(typeof options.keyboardShortcut === "object" ? options.keyboardShortcut : {});
|
|
2516
|
+
}
|
|
2432
2517
|
whenBodyReady(
|
|
2433
2518
|
() => widget.showTrigger(options, () => {
|
|
2434
|
-
|
|
2435
|
-
|
|
2519
|
+
const openOptions = {
|
|
2520
|
+
captureScreenshot: options.captureScreenshot,
|
|
2521
|
+
includeScreenshot: options.includeScreenshot
|
|
2522
|
+
};
|
|
2523
|
+
if (configuration) void FeedbackKit.presentAndSubmit(openOptions);
|
|
2524
|
+
else void FeedbackKit.present({ ...openOptions, onReport: (r) => console.info("[FeedbackKit] report", r) });
|
|
2436
2525
|
})
|
|
2437
2526
|
);
|
|
2438
2527
|
},
|
|
@@ -2447,7 +2536,8 @@ const FeedbackKit = {
|
|
|
2447
2536
|
FeedbackKit.disableKeyboardShortcut();
|
|
2448
2537
|
const key = (options.key ?? "F").toLowerCase();
|
|
2449
2538
|
shortcutHandler = (e) => {
|
|
2450
|
-
|
|
2539
|
+
const matchesKey = e.key.toLowerCase() === key || e.code.toLowerCase() === `key${key}`;
|
|
2540
|
+
if (!(e.metaKey || e.ctrlKey) || !e.shiftKey || e.altKey || !matchesKey) return;
|
|
2451
2541
|
e.preventDefault();
|
|
2452
2542
|
if (configuration) void FeedbackKit.presentAndSubmit();
|
|
2453
2543
|
else void FeedbackKit.present({ onReport: (r) => console.info("[FeedbackKit] report", r) });
|