Skip to content

Commit 7787319

Browse files
committed
feat(pencil): add custom events
1 parent 6ef155d commit 7787319

File tree

13 files changed

+1334
-1239
lines changed

13 files changed

+1334
-1239
lines changed

.releaserc

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414
],
1515
"message": "chore: Release \\${nextRelease.version} [skip ci]\n\n\\${nextRelease.notes}"
1616
}],
17-
["@semantic-release/npm", {
18-
"tarballDir": "./dist"
19-
}]
17+
["@semantic-release/npm"]
2018
]
2119
}

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
Say goodbye to boring audio elments and hello to interactive voice recording! `react-voice-recorder-player` is a React component that lets users record and playback their voice directly in the browser. It even includes a waveform graph that shows the audio being captured and played back in real-time.
99

10-
`react-voice-recorder-player` is an ultra light-weight component. The package size is only `164 kB` meaning it won't add unnecessary bulk to your application. And the best part is, its completely customizable! You can easily change the appearance of the component to fit your application's unique design.
10+
`react-voice-recorder-player` is an ultra light-weight component. The package size is only around `160 kB` meaning it won't add unnecessary bulk to your application. And the best part is, its completely customizable! You can easily change the appearance of the component to fit your application's unique design and trigger your own code on our custom events.
1111

1212
## Installation
1313

@@ -70,6 +70,19 @@ The `VoiceRecorder` component will provide your users with an intuitive MP3-like
7070
| `onAudioDownload` | A callback function that'll get called as soon as the `Blob` is available. | `Function` |
7171
------------------------------------------------------------------------------------------------------------------------------------------
7272

73+
## Custom Events
74+
In addition to the props mentioned above, you can also use our custom callback events to trigger specific actions in your application. Here's a list of the custom events that are available:
75+
76+
| Prop name | Description | Type |
77+
|------------------|-------------------------------------------|-------------|
78+
| `onRecordingStart` | Event triggered when recording starts | `function` |
79+
| `onRecordingEnd` | Event fired when recording ends | `function` |
80+
| `onPlayStart` | Event triggered when playback starts | `function` |
81+
| `onPlayEnd` | Event fired when playback ends | `function` |
82+
| `onRecordingPause` | Event triggered when recording is paused | `function` |
83+
| `onPlayPause` | Event fired when playback is paused | `function` |
84+
85+
7386
## Customization
7487

7588
Want to make the `VoiceRecorder` component even more personalized? No problem! You can customize the appearance of the component by passing in your own style objects. Here's an example:

dist/react-voice-recorder-player.js

Lines changed: 1210 additions & 1175 deletions
Large diffs are not rendered by default.

dist/react-voice-recorder-player.umd.cjs

Lines changed: 15 additions & 15 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "react-voice-recorder-player",
3-
"version": "1.0.10",
3+
"version": "1.1.12",
44
"description": "Voice Recorder Component for React",
55
"repository": {
66
"type": "git",

src/components/controllers/index.tsx

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable jsx-a11y/anchor-is-valid */
22
/* eslint-disable jsx-a11y/anchor-has-content */
33
/// <reference types="vite-plugin-svgr/client" /> // this imports the vite-plugin-svgr declaration file
4-
import { useEffect, useState } from 'react';
4+
import { useEffect, useState, useRef } from 'react';
55
import { useAudio, useUserProps } from '../../context';
66
import { ReactComponent as Pause } from '../../assets/pause.svg';
77
import { ReactComponent as Play } from '../../assets/play.svg';
@@ -20,12 +20,18 @@ const INITIAL_BUTTON_STATUSES = {
2020
};
2121

2222
function Controllers() {
23+
const downloadButtonRef = useRef<HTMLAnchorElement | null>(null);
2324
const { audioStatus, updateAudioStatus, audioRecording } = useAudio();
2425
const {
2526
controllerContainerStyle,
2627
controllerStyle,
27-
downloadable,
28+
downloadable = true,
2829
onAudioDownload,
30+
onRecordingStart,
31+
onPlayStart,
32+
onRecordingPause,
33+
onPlayPause,
34+
rootElementId,
2935
} = useUserProps();
3036
const [buttonStatuses, setButtonStatuses] = useState<Record<string, boolean>>(INITIAL_BUTTON_STATUSES);
3137

@@ -53,26 +59,26 @@ function Controllers() {
5359
.then(updateAudio(RECORDING)).catch(() => alert('Please allow acccess to your microphone to continue.'));
5460
}
5561

56-
useEffect(() => {
57-
const maincontainer = document.querySelector('.voice-recorder_maincontainer') as HTMLElement;
58-
const controlcontainer = document.querySelector('.voice-recorder_controlscontainer') as HTMLElement;
59-
60-
if (maincontainer && controlcontainer) {
61-
const { height } = maincontainer.getBoundingClientRect();
62-
controlcontainer.style.height = `${height / 3}px`;
63-
}
64-
}, []);
65-
6662
const downloadBlob = () => {
6763
const { blob = '' } = audioRecording || {};
68-
if (!blob) return null;
69-
const url = URL.createObjectURL(blob);
70-
const aElem = document.querySelector('.voice-recorder_downloadfile') as HTMLAnchorElement;
71-
aElem.href = url;
72-
aElem.download = 'audio.wav';
73-
aElem.click();
64+
if (!blob || !downloadButtonRef.current) return null;
65+
const url = URL.createObjectURL(blob);
66+
downloadButtonRef.current.href = url;
67+
downloadButtonRef.current.download = 'audio.wav';
68+
downloadButtonRef.current.click();
7469
};
7570

71+
useEffect(() => {
72+
const rootElement = document.getElementById(rootElementId) as HTMLElement;
73+
if (rootElement) {
74+
const controlcontainer = rootElement.querySelector('.voice-recorder_controlscontainer') as HTMLElement;
75+
if (rootElement && controlcontainer) {
76+
const { height } = rootElement.getBoundingClientRect();
77+
controlcontainer.style.height = `${height / 3}px`;
78+
}
79+
}
80+
}, []);
81+
7682
useEffect(() => {
7783
if (audioRecording) {
7884
const { blob = '' } = audioRecording || {};
@@ -89,37 +95,53 @@ function Controllers() {
8995
showRecordBtn: true,
9096
});
9197
break;
92-
case RECORDING:
98+
case RECORDING: {
99+
onRecordingStart?.();
93100
setButtonStatuses({
94101
...INITIAL_BUTTON_STATUSES,
95102
showPauseBtn: true,
96103
showStopBtn: true,
97104
});
98105
break;
99-
case PAUSED_RECORDING:
106+
}
107+
case PAUSED_RECORDING: {
108+
onRecordingPause?.();
100109
setButtonStatuses({
101110
...INITIAL_BUTTON_STATUSES,
102111
showStopBtn: true,
103112
showRecordBtn: true,
104113
});
105114
break;
106-
case PAUSED_PLAYING:
115+
}
116+
case PAUSED_PLAYING: {
117+
onPlayPause?.();
107118
setButtonStatuses({
108119
...INITIAL_BUTTON_STATUSES,
109120
showPlayBtn: true,
110121
showPauseBtn: true,
111122
showRedoBtn: true,
112123
});
113124
break;
114-
case PLAYING:
115-
case STOPPED:
125+
}
126+
case PLAYING: {
127+
onPlayStart?.();
128+
setButtonStatuses({
129+
...INITIAL_BUTTON_STATUSES,
130+
showPauseBtn: true,
131+
showPlayBtn: true,
132+
showRedoBtn: true,
133+
});
134+
break;
135+
}
136+
case STOPPED: {
116137
setButtonStatuses({
117138
...INITIAL_BUTTON_STATUSES,
118139
showPauseBtn: true,
119140
showPlayBtn: true,
120141
showRedoBtn: true,
121142
});
122143
break;
144+
}
123145
default:
124146
setButtonStatuses(INITIAL_BUTTON_STATUSES);
125147
}
@@ -171,7 +193,7 @@ function Controllers() {
171193
})}
172194
</div>
173195
</div>
174-
<a download style={{display: "none"}} className="voice-recorder_downloadfile" />
196+
<a ref={downloadButtonRef} download style={{display: "none"}} className="voice-recorder_downloadfile" />
175197
</div>
176198
)
177199
}

src/components/waveform/playback.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const BUFFER_VS_HTML_DURATION_DIFFERENCE = 0.06;
88
function Playback() {
99
const [adjustedBars, setAdjustedBars] = useState<GraphDataType[]>([]);
1010
const { audioRecording, audioStatus, updateAudioStatus } = useAudio();
11-
const { graphColor, graphShaded } = useUserProps();
11+
const { graphColor, graphShaded, rootElementId, onPlayEnd } = useUserProps();
1212
const unplayedCanvasRef = useRef<HTMLCanvasElement | null>(null);
1313
const playedCanvasRef = useRef<HTMLCanvasElement | null>(null);
1414
const progressLineRef = useRef<HTMLCanvasElement | null>(null);
@@ -64,21 +64,24 @@ function Playback() {
6464
};
6565

6666
const setUpCanvas = () => {
67-
const canvasElements = setUpCanvasUtil([
67+
const canvasElements = setUpCanvasUtil(
68+
rootElementId,
69+
[
6870
'waveformgraph-unplayed-graph',
6971
'waveformgraph-played-graph',
7072
'progressbar',
7173
], '.voice-recorder_canvascontainer');
7274

73-
unplayedCanvasRef.current = canvasElements?.find((elem) => elem.id === 'waveformgraph-unplayed-graph') as HTMLCanvasElement;
74-
playedCanvasRef.current = canvasElements?.find((elem) => elem.id === 'waveformgraph-played-graph') as HTMLCanvasElement;
75-
progressLineRef.current = canvasElements?.find((elem) => elem.id === 'progressbar') as HTMLCanvasElement;
75+
unplayedCanvasRef.current = canvasElements?.find((elem) => elem.className === 'waveformgraph-unplayed-graph') as HTMLCanvasElement;
76+
playedCanvasRef.current = canvasElements?.find((elem) => elem.className === 'waveformgraph-played-graph') as HTMLCanvasElement;
77+
progressLineRef.current = canvasElements?.find((elem) => elem.className === 'progressbar') as HTMLCanvasElement;
7678
}
7779

7880
const pauseAudio = () => audioRef?.current?.pause();
7981

8082
const playAudio = () => {
81-
const audioELem = document.querySelector('#playback_audio') as HTMLAudioElement;
83+
const rootElement = document.getElementById(rootElementId) as HTMLElement;
84+
const audioELem = rootElement.querySelector('#playback_audio') as HTMLAudioElement;
8285
audioELem.autoplay = true;
8386
/* play from start */
8487
if (audioELem.readyState !== 4) {
@@ -100,6 +103,7 @@ function Playback() {
100103
}
101104
}
102105
audioELem.onended = () => {
106+
if (blob) onPlayEnd?.(blob);
103107
updateAudioStatus(STOPPED);
104108
}
105109
};

src/components/waveform/record.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const GRAPH_WIDTH = 2;
1313

1414
function Record() {
1515
const { audioStatus = '', updateAudioRecording } = useAudio();
16-
const { graphColor, graphShaded } = useUserProps();
16+
const { graphColor, graphShaded, rootElementId, onRecordingEnd } = useUserProps();
1717
const [now, setNow] = useState<number>(0);
1818
const canvasRef = useRef<HTMLCanvasElement | null>(null);
1919
const obj = useRef<CanvasObj>({});
@@ -48,7 +48,7 @@ function Record() {
4848
}, []);
4949

5050
const setUpCanvas = () => {
51-
const canvas = setUpCanvasUtil(['waveformgraph-record'], '.voice-recorder_recordcontainer')
51+
const canvas = setUpCanvasUtil(rootElementId, ['waveformgraph-record'], '.voice-recorder_recordcontainer');
5252

5353
if (canvas) canvasRef.current = canvas[0];
5454
};
@@ -90,6 +90,7 @@ function Record() {
9090
const arrayBuffer = await event.data.arrayBuffer();
9191
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
9292
const recordingData = { blob: event.data, duration: audioBuffer.duration, graphData: (obj.current.graphData ?? []) }
93+
onRecordingEnd?.(event.data);
9394
updateAudioRecording(recordingData);
9495
});
9596
mediaRecorder.addEventListener('stop', () => {
@@ -124,7 +125,7 @@ function Record() {
124125

125126
maxFreq = Math.max(0, ...obj?.current?.dataArray ?? [])
126127

127-
const freq = Math.max(1, Math.floor(maxFreq * 350));
128+
const freq = Math.max(1, Math.floor(maxFreq * 550));
128129

129130
if (obj.current.graphData === undefined) {
130131
obj.current.graphData = [];

src/context/user-props.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ const UserPropsContext = createContext<UserPropsContextInterface>({
1111
width: 'inherit',
1212
downloadable: false,
1313
onAudioDownload: () => undefined,
14+
onRecordingStart: () => undefined,
15+
onRecordingEnd: () => undefined,
16+
onPlayStart: () => undefined,
17+
onPlayEnd: () => undefined,
18+
onRecordingPause: () => undefined,
19+
onPlayPause: () => undefined,
20+
rootElementId: '',
1421
});
1522

1623
function UserPropsProvider({ userPropsValue, children } :

src/styles/waveform.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
position: relative;
4343
height: 100%;
4444

45-
#progressbar, #waveformgraph-played-graph {
45+
.progressbar, .waveformgraph-played-graph {
4646
position: absolute;
4747
left: 0;
4848
}

0 commit comments

Comments
 (0)