dictate-button
Version:
Customizable Web Component that adds speech-to-text dictation capabilities to text fields
141 lines (140 loc) • 5.26 kB
JavaScript
function v(t, c = {}) {
const { buttonSize: n = 30, verbose: i = !1, customApiEndpoint: a } = c, l = document.querySelectorAll(t);
for (const e of l) {
if (e.hasAttribute("data-dictate-button-enabled")) continue;
const p = e.parentNode;
if (!e.isConnected || !p) {
i && console.debug("injectDictateButton: skipping detached field", e);
continue;
}
e.setAttribute("data-dictate-button-enabled", "");
const r = document.createElement("div");
r.style.position = "relative";
const g = getComputedStyle(e), s = g.display === "block";
r.style.display = s ? "block" : "inline-block", r.style.width = s ? "100%" : "auto", r.style.color = "inherit", r.classList.add("dictate-button-wrapper"), p.insertBefore(r, e), r.appendChild(e), r.style.margin = g.margin, e.style.margin = "0", e.style.boxSizing = "border-box";
const u = m(g);
e.style.paddingRight = `${n + u * 2}px`;
const o = document.createElement("dictate-button");
o.size = n, o.style.position = "absolute", o.style.right = "0", o.style.top = E(
r,
g,
e.tagName,
n
) + "px", o.style.marginRight = o.style.marginLeft = `${u}px`, o.style.marginTop = "0", o.style.marginBottom = "0", a && (o.apiEndpoint = a), o.language = h(), o.addEventListener("recording:started", (d) => {
i && console.debug("recording:started", d);
}), o.addEventListener("recording:stopped", (d) => {
i && console.debug("recording:stopped", d);
}), o.addEventListener("recording:failed", (d) => {
i && console.debug("recording:failed", d), f(e);
}), o.addEventListener("transcribing:started", (d) => {
i && console.debug("transcribing:started", d);
}), o.addEventListener("transcribing:finished", (d) => {
i && console.debug("transcribing:finished", d);
const b = d.detail;
T(e, b);
}), o.addEventListener("transcribing:failed", (d) => {
i && console.debug("transcribing:failed", d), f(e);
}), r.appendChild(o);
}
}
function h() {
const t = document.documentElement.lang;
if (t && t.length >= 2)
try {
return (Intl?.Locale ? new Intl.Locale(t) : null)?.language ?? t.split(/[-_]/)[0].toLowerCase();
} catch {
return t.split(/[-_]/)[0].toLowerCase();
}
}
function E(t, c, n, i) {
if (n.toLowerCase() === "textarea") {
const l = parseFloat(c.paddingTop || "0");
return Math.max(4, l);
}
const a = Math.round(t.clientHeight / 2 - i / 2);
return Math.max(4, a);
}
function m(t) {
const c = parseFloat(t.paddingRight || "0");
return Math.max(c, 4);
}
function T(t, c) {
const n = typeof c == "string" ? c.trim() : String(c ?? "").trim();
n.length !== 0 && (y(t) ? N(t, n) : C(t, n), t.dispatchEvent(new Event("input", { bubbles: !0, composed: !0 })), f(t));
}
function f(t) {
try {
t.focus({ preventScroll: !0 });
} catch {
t.focus();
}
}
function y(t) {
return t.isContentEditable;
}
function C(t, c) {
const n = t.selectionStart ?? 0, i = t.selectionEnd ?? 0, a = n > 0 ? t.value.charAt(n - 1) : "", l = a && !/\s/.test(a), e = i < t.value.length ? t.value.charAt(i) : "", p = e && !/\s/.test(e), r = (l ? " " : "") + c + (p ? " " : ""), g = n + r.length, s = typeof t.scrollTop == "number" ? t.scrollTop : null;
if (typeof t.setRangeText == "function")
t.setRangeText(r, n, i, "end");
else {
t.value = t.value.substring(0, n) + r + t.value.substring(i);
try {
t.selectionStart = g, t.selectionEnd = g;
} catch {
}
}
s !== null && (t.scrollTop = s);
}
function N(t, c) {
const n = window.getSelection();
if (!(n && n.rangeCount > 0 && t.contains(n.getRangeAt(0).commonAncestorContainer))) {
f(t);
const l = document.createRange();
l.selectNodeContents(t), l.collapse(!1), n?.removeAllRanges(), n?.addRange(l);
}
const a = n?.getRangeAt(0);
if (a) {
const l = a.cloneRange(), e = a.cloneRange();
let p = !1;
l.collapse(!0);
try {
l.setStart(a.startContainer, 0);
const s = l.toString(), u = s.length > 0 ? s.charAt(s.length - 1) : "";
p = u !== "" && !/\s/.test(u);
} catch (s) {
console.debug(
"insertIntoContentEditable: Error checking text before cursor:",
s
);
}
let r = !1;
e.collapse(!1);
try {
if (e.endContainer.nodeType === Node.TEXT_NODE) {
const o = e.endContainer;
e.setEnd(o, o.length);
} else if (e.endContainer.nodeType === Node.ELEMENT_NODE) {
const o = e.endContainer;
o.childNodes.length > e.endOffset && e.setEnd(o, e.endOffset + 1);
}
const s = e.toString(), u = s.length > 0 ? s.charAt(0) : "";
r = u !== "" && !/\s/.test(u);
} catch (s) {
console.debug(
"insertIntoContentEditable: Error checking text after cursor:",
s
);
}
const g = (p ? " " : "") + c + (r ? " " : "");
try {
a.deleteContents();
const s = document.createTextNode(g);
a.insertNode(s), a.setStartAfter(s), a.setEndAfter(s), n?.removeAllRanges(), n?.addRange(a);
} catch (s) {
console.debug("insertIntoContentEditable: Error inserting text:", s), f(t), t.textContent = (t.textContent || "") + g;
}
}
}
export {
v as injectDictateButton
};