react-text-to-speech
Advanced tools
Comparing version 0.13.5 to 0.14.0
@@ -1,25 +0,9 @@ | ||
import React, { DetailedHTMLProps, HTMLAttributes } from "react"; | ||
import { QueueChangeEventHandler } from "./queue.js"; | ||
export type SpanProps = DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>; | ||
export type SpeechSynthesisErrorHandler = (error: Error) => any; | ||
export type SpeechSynthesisEventHandler = (event: SpeechSynthesisEvent) => any; | ||
export type useSpeechProps = { | ||
text: string | JSX.Element; | ||
pitch?: number; | ||
rate?: number; | ||
volume?: number; | ||
lang?: string; | ||
voiceURI?: string | string[]; | ||
highlightText?: boolean; | ||
highlightProps?: SpanProps; | ||
preserveUtteranceQueue?: boolean; | ||
onError?: SpeechSynthesisErrorHandler; | ||
onStart?: SpeechSynthesisEventHandler; | ||
onResume?: SpeechSynthesisEventHandler; | ||
onPause?: SpeechSynthesisEventHandler; | ||
onStop?: SpeechSynthesisEventHandler; | ||
onBoundary?: SpeechSynthesisEventHandler; | ||
onQueueChange?: QueueChangeEventHandler; | ||
import React from "react"; | ||
import { dequeue, clearQueueHook } from "./queue.js"; | ||
import { SpeechStatus, useSpeechProps, SpeechUtterancesQueue } from "./types.js"; | ||
export declare function useQueue(): { | ||
queue: SpeechUtterancesQueue; | ||
dequeue: typeof dequeue; | ||
clearQueue: typeof clearQueueHook; | ||
}; | ||
export type SpeechStatus = "started" | "paused" | "stopped" | "queued"; | ||
export declare function useSpeech({ text, pitch, rate, volume, lang, voiceURI, highlightText, highlightProps, preserveUtteranceQueue, onError, onStart, onResume, onPause, onStop, onBoundary, onQueueChange, }: useSpeechProps): { | ||
@@ -26,0 +10,0 @@ Text: () => React.ReactNode; |
import React, { cloneElement, isValidElement, useEffect, useMemo, useRef, useState } from "react"; | ||
import { ArrayToText, JSXToArray, findCharIndex, getIndex, isParent, sanitize } from "./utils.js"; | ||
import { addToQueue, clearQueue, removeFromQueue, speakFromQueue } from "./queue.js"; | ||
function useStateRef(init) { | ||
const [state, setState] = useState(init); | ||
const ref = useRef(init); | ||
function setStateRef(value) { | ||
ref.current = value; | ||
setState(value); | ||
} | ||
return [state, ref, setStateRef]; | ||
import { addToQueue, removeFromQueue, speakFromQueue, subscribe, dequeue, clearQueueHook, clearQueueUnload, clearQueue } from "./queue.js"; | ||
import { ArrayToText, JSXToArray, cancel, findCharIndex, getIndex, isParent, sanitize } from "./utils.js"; | ||
export function useQueue() { | ||
const [queue, setQueue] = useState([]); | ||
useEffect(() => subscribe((queue) => setQueue([...queue])), []); | ||
return { queue, dequeue, clearQueue: clearQueueHook }; | ||
} | ||
@@ -17,3 +13,2 @@ export function useSpeech({ text, pitch = 1, rate = 1, volume = 1, lang, voiceURI, highlightText = false, highlightProps = { style: { backgroundColor: "yellow" } }, preserveUtteranceQueue = false, onError = console.error, onStart, onResume, onPause, onStop, onBoundary, onQueueChange, }) { | ||
const utteranceRef = useRef(); | ||
const cancel = () => { var _a; return (_a = window.speechSynthesis) === null || _a === void 0 ? void 0 : _a.cancel(); }; | ||
const [words, stringifiedWords] = useMemo(() => { | ||
@@ -51,3 +46,3 @@ const words = JSXToArray(text); | ||
const stopEventHandler = (event) => { | ||
window.removeEventListener("beforeunload", cancel); | ||
window.removeEventListener("beforeunload", clearQueueUnload); | ||
setSpeechStatus("stopped"); | ||
@@ -66,3 +61,3 @@ setSpeakingWord(null); | ||
utterance.onstart = (event) => { | ||
window.addEventListener("beforeunload", cancel); | ||
window.addEventListener("beforeunload", clearQueueUnload); | ||
setSpeechStatus("started"); | ||
@@ -88,3 +83,3 @@ onStart === null || onStart === void 0 ? void 0 : onStart(event); | ||
clearQueue(); | ||
addToQueue(utterance, onQueueChange); | ||
addToQueue({ utterance, setSpeechStatus }, onQueueChange); | ||
if (synth.speaking) { | ||
@@ -148,1 +143,10 @@ if (preserveUtteranceQueue && speechStatus !== "started") { | ||
} | ||
function useStateRef(init) { | ||
const [state, setState] = useState(init); | ||
const ref = useRef(init); | ||
function setStateRef(value) { | ||
ref.current = value; | ||
setState(value); | ||
} | ||
return [state, ref, setStateRef]; | ||
} |
import React from "react"; | ||
export type IconProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>; | ||
import { IconProps } from "./types.js"; | ||
export declare function HiMiniStop(props: IconProps): React.JSX.Element; | ||
export declare function HiVolumeOff(props: IconProps): React.JSX.Element; | ||
export declare function HiVolumeUp(props: IconProps): React.JSX.Element; | ||
export declare function HiVolumeOff(props: IconProps): React.JSX.Element; | ||
export declare function HiMiniStop(props: IconProps): React.JSX.Element; |
import React from "react"; | ||
export function HiVolumeUp(props) { | ||
export function HiMiniStop(props) { | ||
return (React.createElement("span", Object.assign({}, props), | ||
React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true, width: "1.25rem", height: "1.25rem" }, | ||
React.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" })))); | ||
React.createElement("path", { d: "M5.25 3A2.25 2.25 0 003 5.25v9.5A2.25 2.25 0 005.25 17h9.5A2.25 2.25 0 0017 14.75v-9.5A2.25 2.25 0 0014.75 3h-9.5z" })))); | ||
} | ||
@@ -12,6 +12,6 @@ export function HiVolumeOff(props) { | ||
} | ||
export function HiMiniStop(props) { | ||
export function HiVolumeUp(props) { | ||
return (React.createElement("span", Object.assign({}, props), | ||
React.createElement("svg", { viewBox: "0 0 20 20", fill: "currentColor", "aria-hidden": true, width: "1.25rem", height: "1.25rem" }, | ||
React.createElement("path", { d: "M5.25 3A2.25 2.25 0 003 5.25v9.5A2.25 2.25 0 005.25 17h9.5A2.25 2.25 0 0017 14.75v-9.5A2.25 2.25 0 0014.75 3h-9.5z" })))); | ||
React.createElement("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM14.657 2.929a1 1 0 011.414 0A9.972 9.972 0 0119 10a9.972 9.972 0 01-2.929 7.071 1 1 0 01-1.414-1.414A7.971 7.971 0 0017 10c0-2.21-.894-4.208-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 10a5.984 5.984 0 01-1.757 4.243 1 1 0 01-1.415-1.415A3.984 3.984 0 0013 10a3.983 3.983 0 00-1.172-2.828 1 1 0 010-1.415z" })))); | ||
} |
@@ -1,24 +0,6 @@ | ||
import React, { DetailedHTMLProps, HTMLAttributes, ReactNode } from "react"; | ||
import { SpeechStatus, useSpeech, useSpeechProps } from "./hooks.js"; | ||
export type Button = JSX.Element | string | null; | ||
export type DivProps = DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>; | ||
export type ChildrenOptions = { | ||
speechStatus?: SpeechStatus; | ||
isInQueue?: boolean; | ||
start?: Function; | ||
pause?: Function; | ||
stop?: Function; | ||
}; | ||
export type Children = (childrenOptions: ChildrenOptions) => ReactNode; | ||
export type SpeechProps = useSpeechProps & { | ||
id?: string; | ||
startBtn?: Button; | ||
pauseBtn?: Button; | ||
stopBtn?: Button; | ||
useStopOverPause?: boolean; | ||
props?: DivProps; | ||
children?: Children; | ||
}; | ||
export { useSpeech }; | ||
import React from "react"; | ||
import { useSpeech, useQueue } from "./hooks.js"; | ||
import { DivProps, SpeechProps } from "./types.js"; | ||
export declare function HighlightedText({ id, children, ...props }: DivProps): React.JSX.Element; | ||
export default function Speech({ id, startBtn, pauseBtn, stopBtn, useStopOverPause, props, children, ...hookProps }: SpeechProps): React.JSX.Element; | ||
export declare function HighlightedText({ id, children, ...props }: DivProps): React.JSX.Element; | ||
export { useSpeech, useQueue }; |
@@ -14,5 +14,12 @@ var __rest = (this && this.__rest) || function (s, e) { | ||
import { createPortal } from "react-dom"; | ||
import { useSpeech } from "./hooks.js"; | ||
import { useSpeech, useQueue } from "./hooks.js"; | ||
import { HiMiniStop, HiVolumeOff, HiVolumeUp } from "./icons.js"; | ||
export { useSpeech }; | ||
export function HighlightedText(_a) { | ||
var { id, children } = _a, props = __rest(_a, ["id", "children"]); | ||
const [loading, setLoading] = useState(true); | ||
useEffect(() => { | ||
setLoading(false); | ||
}, []); | ||
return (React.createElement("div", Object.assign({ id: `rtts-${id}` }, props), loading && (typeof children === "string" ? React.createElement("span", null, children) : children))); | ||
} | ||
export default function Speech(_a) { | ||
@@ -35,9 +42,2 @@ var { id, startBtn = React.createElement(HiVolumeUp, null), pauseBtn = React.createElement(HiVolumeOff, null), stopBtn = React.createElement(HiMiniStop, null), useStopOverPause = false, props = {}, children } = _a, hookProps = __rest(_a, ["id", "startBtn", "pauseBtn", "stopBtn", "useStopOverPause", "props", "children"]); | ||
} | ||
export function HighlightedText(_a) { | ||
var { id, children } = _a, props = __rest(_a, ["id", "children"]); | ||
const [loading, setLoading] = useState(true); | ||
useEffect(() => { | ||
setLoading(false); | ||
}, []); | ||
return (React.createElement("div", Object.assign({ id: `rtts-${id}` }, props), loading && (typeof children === "string" ? React.createElement("span", null, children) : children))); | ||
} | ||
export { useSpeech, useQueue }; |
@@ -1,6 +0,11 @@ | ||
export type SpeechUtterancesQueue = SpeechSynthesisUtterance[]; | ||
export type QueueChangeEventHandler = (queue: SpeechUtterancesQueue) => any; | ||
export declare function addToQueue(utterance: SpeechSynthesisUtterance, callback?: QueueChangeEventHandler): void; | ||
import { QueueChangeEventHandler, SpeechQueueItem } from "./types.js"; | ||
export declare function clearQueue(start?: number, emitEvent?: boolean): void; | ||
export declare function addToQueue(item: SpeechQueueItem, callback?: QueueChangeEventHandler): void; | ||
export declare function clearQueueHook(): void; | ||
export declare function clearQueueUnload(): void; | ||
export declare function dequeue(index?: number): void; | ||
export declare function emit(callback?: QueueChangeEventHandler): void; | ||
export declare function removeFromQueue(utterance: SpeechSynthesisUtterance, callback?: QueueChangeEventHandler): void; | ||
export declare function clearQueue(): void; | ||
export declare function removeFromQueue(utterance: number): void; | ||
export declare function speakFromQueue(): void; | ||
export declare function subscribe(callback: QueueChangeEventHandler): () => void; |
@@ -1,20 +0,55 @@ | ||
let queue = []; | ||
export function addToQueue(utterance, callback) { | ||
queue.push(utterance); | ||
callback === null || callback === void 0 ? void 0 : callback(queue); | ||
import { cancel } from "./utils.js"; | ||
const queue = []; | ||
const queueListeners = []; | ||
export function clearQueue(start = 0, emitEvent = false) { | ||
queue.slice(start).forEach(({ setSpeechStatus }) => setSpeechStatus("stopped")); | ||
queue.length = 0; | ||
if (emitEvent) | ||
emit(); | ||
} | ||
export function addToQueue(item, callback) { | ||
queue.push(item); | ||
emit(callback); | ||
} | ||
export function clearQueueHook() { | ||
cancel(); | ||
clearQueue(1, true); | ||
} | ||
export function clearQueueUnload() { | ||
cancel(); | ||
clearQueue(1); | ||
} | ||
export function dequeue(index = 0) { | ||
if (index === 0) | ||
cancel(); | ||
else | ||
removeFromQueue(index); | ||
} | ||
export function emit(callback) { | ||
const utteranceQueue = queue.map(({ utterance }) => utterance); | ||
queueListeners.forEach((listener) => listener(utteranceQueue)); | ||
callback === null || callback === void 0 ? void 0 : callback(utteranceQueue); | ||
} | ||
export function removeFromQueue(utterance, callback) { | ||
const index = queue.indexOf(utterance); | ||
const index = typeof utterance === "number" ? utterance : queue.findIndex((item) => item.utterance === utterance); | ||
if (index === -1) | ||
return; | ||
queue.splice(index, 1); | ||
callback === null || callback === void 0 ? void 0 : callback(queue); | ||
const [item] = queue.splice(index, 1); | ||
if (item) { | ||
item.setSpeechStatus("stopped"); | ||
emit(callback); | ||
} | ||
} | ||
export function clearQueue() { | ||
queue = []; | ||
} | ||
export function speakFromQueue() { | ||
const utterance = queue[0]; | ||
if (utterance) | ||
speechSynthesis.speak(utterance); | ||
const item = queue[0]; | ||
if (item) | ||
speechSynthesis.speak(item.utterance); | ||
} | ||
export function subscribe(callback) { | ||
queueListeners.push(callback); | ||
return () => { | ||
const index = queueListeners.indexOf(callback); | ||
if (index !== -1) | ||
queueListeners.splice(index, 1); | ||
}; | ||
} |
import { ReactNode } from "react"; | ||
export type Index = string | number; | ||
export type StringArray = string | StringArray[]; | ||
export declare const getIndex: (parentIndex: Index, index: Index) => string; | ||
export declare const sanitize: (text: string) => string; | ||
import { Index, StringArray } from "./types.js"; | ||
export declare function ArrayToText(element: StringArray): string; | ||
export declare function JSXToArray(element: ReactNode): StringArray; | ||
export declare function ArrayToText(element: StringArray): string; | ||
export declare function cancel(): void; | ||
export declare function findCharIndex(words: StringArray, index: number): string; | ||
export declare function getIndex(parentIndex: Index, index: Index): string; | ||
export declare function isParent(parentIndex: string, index?: string): boolean; | ||
export declare const sanitize: (text: string) => string; |
import { isValidElement } from "react"; | ||
export const getIndex = (parentIndex, index) => `${parentIndex === "" ? "" : parentIndex + "-"}${index}`; | ||
export const sanitize = (text) => text.replace(/[;<>]/g, (match) => (match === ">" ? ")" : "(")); | ||
export function ArrayToText(element) { | ||
if (typeof element === "string") | ||
return element; | ||
return element.map(ArrayToText).join(" ") + " "; | ||
} | ||
export function JSXToArray(element) { | ||
@@ -13,6 +16,6 @@ if (isValidElement(element)) { | ||
} | ||
export function ArrayToText(element) { | ||
if (typeof element === "string") | ||
return element; | ||
return element.map(ArrayToText).join(" ") + " "; | ||
export function cancel() { | ||
var _a; | ||
if (typeof window !== "undefined") | ||
(_a = window.speechSynthesis) === null || _a === void 0 ? void 0 : _a.cancel(); | ||
} | ||
@@ -37,2 +40,5 @@ export function findCharIndex(words, index) { | ||
} | ||
export function getIndex(parentIndex, index) { | ||
return `${parentIndex === "" ? "" : parentIndex + "-"}${index}`; | ||
} | ||
export function isParent(parentIndex, index) { | ||
@@ -51,1 +57,2 @@ if (!(index === null || index === void 0 ? void 0 : index.startsWith(parentIndex))) | ||
} | ||
export const sanitize = (text) => text.replace(/[;<>]/g, (match) => (match === ">" ? ")" : "(")); |
{ | ||
"name": "react-text-to-speech", | ||
"version": "0.13.5", | ||
"version": "0.14.0", | ||
"description": "An easy to use react component for the Web Speech API.", | ||
@@ -41,4 +41,4 @@ "type": "module", | ||
"@types/react": "^18.2.74", | ||
"@types/react-dom": "^18.2.23" | ||
"@types/react-dom": "^18.2.24" | ||
} | ||
} |
@@ -167,13 +167,11 @@ # react-text-to-speech | ||
The `preserveUtteranceQueue` prop, available in both `useSpeech` hook and `<Speech>` component, facilitates handling multiple speech instances simultaneously. | ||
If set to `false`, any currently speaking speech utterance will be stopped immediately upon initiating a new utterance. The new utterance will be spoken without waiting for the previous one to finish. | ||
The `preserveUtteranceQueue` prop, available in both `useSpeech` hook and `<Speech>` component, facilitates handling multiple speech instances simultaneously.\ | ||
If set to `false`, any currently speaking speech utterance will be stopped immediately upon initiating a new utterance. The new utterance will be spoken without waiting for the previous one to finish.\ | ||
If set to `true`, new speech utterances will be added to a queue. They will be spoken once the previous speech instances are completed. This allows for sequential delivery of speech content, maintaining order and avoiding overlapping utterances. | ||
`TIP`: The `onQueueChange` event handler used [above](#handling-errors-and-events) runs whenever queue updates. | ||
The `useQueue` hook can be used to keep track of the queue as well as change the queue as shown below. The `onQueueChange` event handler used [above](#handling-errors-and-events) can also be used to keep track of queue updates. | ||
```jsx | ||
import React, { useMemo } from "react"; | ||
import { useSpeech } from "react-text-to-speech"; | ||
import { useQueue, useSpeech } from "../components/dist"; | ||
@@ -207,8 +205,28 @@ function NewsItem({ title, desc }) { | ||
]; | ||
const { | ||
queue, // Queue that stores all the speech utterances | ||
clearQueue, // Function to clear the queue | ||
dequeue, // Function to remove a speech utterance from the queue at a specific index | ||
} = useQueue(); | ||
return ( | ||
<div style={{ display: "flex", flexDirection: "column", rowGap: "1rem" }}> | ||
{news.map(({ id, title, desc }) => ( | ||
<NewsItem key={id} title={title} desc={desc} /> | ||
))} | ||
<div> | ||
<div style={{ display: "flex", flexDirection: "column", rowGap: "1rem" }}> | ||
{news.map(({ id, title, desc }) => ( | ||
<NewsItem key={id} title={title} desc={desc} /> | ||
))} | ||
</div> | ||
<div style={{ display: "flex", flexDirection: "column", rowGap: "1rem", marginBlock: "2rem" }}> | ||
{queue.length ? ( | ||
queue.map(({ text }, i) => ( | ||
<div key={i}> | ||
<button onClick={() => dequeue(i)}>Dequeue</button> | ||
<span style={{ marginLeft: "1rem" }}>{text}</span> | ||
</div> | ||
)) | ||
) : ( | ||
<div>Queue is Empty</div> | ||
)} | ||
</div> | ||
<button onClick={clearQueue}>Clear Queue</button> | ||
</div> | ||
@@ -215,0 +233,0 @@ ); |
37922
15
418
438