How to clear a sequence of audio with timeouts in React - javascript

I have set 4 timeout for audios in my application and I need to stop the audios after user click. The macro function is working correctly, however the clearTimout does not stop the sound. Anyone knows how to clear it?
export function handlePlay(audio) {
audio.currentTime = 0;
return audio.play();
}
export function handleConversation(clear) {
const timer1 = () => setTimeout(() => {
handlePlay(conversation[Math.floor(Math.random() * conversation.length)]);
}, TIME1);
const timer2 = () => setTimeout(() => {
handlePlay(conversation[Math.floor(Math.random() * conversation.length)]);
}, TIME2);
const timer3 = () => setTimeout(() => {
handlePlay(conversation[Math.floor(Math.random() * conversation.length)]);
}, TIME3);
const timer4 = () => setTimeout(() => {
handlePlay(conversation[Math.floor(Math.random() * conversation.length)]);
}, TIME4);
if (clear) {
console.log('enter clear');
return () => {
clearTimeout(timer1);
clearTimeout(timer2);
clearTimeout(timer3);
clearTimeout(timer4);
};
}
timer1();
timer2();
timer3();
timer4();
}

after clearTimeouts call this code
audio.pause();
audio.currentTime = 0;

Here a suggestion of what you could do.
I guess this could be improved further regarding how this handleConversation function is used, I didn't really get the whole idea and there is still some inconsistencies...
function createAudio(track) {
track.audio = conversation[Math.floor(Math.random() * conversation.length)];
}
export class Track {
constructor(time) {
this.time = time;
this.timeoutid = 0;
this.audio = new Audio();
}
timer() {
this.timeoutid = setTimeout(() => {
createAudio(this);
handlePlay(this.audio);
}, TIME1);
}
play() {
this.audio.currentTime = 0;
this.audio.play();
}
stop() {
this.audio.pause();
this.audio.currentTime = 0;
clearTimeout(this.timeoutid)
}
}
export function handleConversation(clear) {
const track1 = new Track(TIME1);
const track2 = new Track(TIME2);
const track3 = new Track(TIME3);
const track4 = new Track(TIME4);
// this part actually doesn't make a lot of sense, since all tracks will be recreated each time the function is called.
// I don't really understand how this is used.
// I imagine the tracks should more likey be stored outside the function in a persistent object.
// I will modify my answer if you provide more details about how you use handleConversation
if (clear) {
console.log('enter clear');
return () => {
[track1, track2, track3, track4].forEach(track => {
track.stop()
});
};
}
[track1, track2, track3, track4].forEach(track => {
track.timer()
});
}

Related

Disable eventListener for 1 second after invocation

I have an eventListener added to the window that activates code that takes 1000ms to run. Because of this, if you click again in less than 1000ms, it breaks. How can I disable the eventListener from running again within 1000ms of its previous invocation? I have a feeling it may have something to do with Date() but I'm not sure how to implement it.
Here's my code:
window.addEventListener("mousedown", event => {
let rememberContent = event.target.innerHTML;
if (event.target.classList.contains('color')) {
navigator.clipboard.writeText(event.target.dataset.hex);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
}, 1000)
} else {
navigator.clipboard.writeText(event.target.innerHTML);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
}, 1000)
}
})
I realize this question has been asked in multiple forms around the internet but I'm struggling to find a vanilla JS solution that works for me. That said, if an answer exists somewhere that I missed, feel free to point me in that direction!
It's called throttling, might be useful to search with that word
let working = false
window.addEventListener("mousedown", event => {
if (working) return
working = true
let rememberContent = event.target.innerHTML;
if (event.target.classList.contains('color')) {
navigator.clipboard.writeText(event.target.dataset.hex);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
working = false
}, 1000)
} else {
navigator.clipboard.writeText(event.target.innerHTML);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
working = false
}, 1000)
}
})
You can remove the listener and add it back after 1 second.
window.addEventListener("mousedown", handleClick)
function handleClick(event) {
let rememberContent = event.target.innerHTML;
if (event.target.classList.contains('color')) {
navigator.clipboard.writeText(event.target.dataset.hex);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
}, 1000)
} else {
navigator.clipboard.writeText(event.target.innerHTML);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
}, 1000)
}
window.removeEventListener("mousedown", handleClick);
setTimeout(() => window.addEventListener("mousedown", handleClick), 1000)
}
If you have a button, I recommend visually disabling it while the event listener is removed.
Maybe just add new variable that stores information about state of the process.
For example:
let isRunning = false;
window.addEventListener("mousedown", event => {
if (isRunning) return;
isRunning = true;
let rememberContent = event.target.innerHTML;
if (event.target.classList.contains('color')) {
navigator.clipboard.writeText(event.target.dataset.hex);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
isRunning = false;
}, 1000)
} else {
navigator.clipboard.writeText(event.target.innerHTML);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
isRunning = false;
}, 1000)
}
})
If isRunning is true, exit early.
You can set a global variable that controls whether or not clicks will fire your entire function. Then, you can pause events at the start of your function, and unpause events at the end of the wait period.
let pauseWindowEvent = false;
window.addEventListener("mousedown", event => {
if (pauseWindowEvent) return false;
pauseWindowEvent = true
let rememberContent = event.target.innerHTML;
if (event.target.classList.contains('color')) {
navigator.clipboard.writeText(event.target.dataset.hex);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
pauseWindowEvent = false;
}, 1000)
} else {
navigator.clipboard.writeText(event.target.innerHTML);
event.target.innerHTML = "<span>copied!</span>";
setTimeout(() => {
event.target.innerHTML = rememberContent;
pauseWindowEvent = false;
}, 1000)
}
})

Why I can't acces this property in my class

I am trying to use this class to play and pause an interval timer but I am having problems with pause(). I don't know why I can't acces the timerId property from pause().
this.timerId return null in pause() . What am I doing wrong?
class IntervalTimer {
callbackStartTime;
remaining = 0;
paused = false;
timerId = null;
_callback;
_delay;
constructor(callback, delay) {
this._callback = callback;
this._delay = delay;
}
pause() {
console.log("this.timerId", this.timerId); //return null
if (!this.paused) {
this.clear();
this.remaining = new Date().getTime() - this.callbackStartTime;
this.paused = true;
}
}
resume() {
if (this.paused) {
if (this.remaining) {
setTimeout(() => {
this.run();
this.paused = false;
this.start();
}, this.remaining);
} else {
this.paused = false;
this.start();
}
}
}
clear() {
clearInterval(this.timerId);
}
start() {
console.log("this.timerId", this.timerId); // return timerId correctly
this.clear();
this.timerId = setInterval(() => {
this.run();
}, this._delay);
}
run() {
this.callbackStartTime = new Date().getTime();
this._callback();
}
}
export default IntervalTimer;
Here my code where I use the class
const interval = new IntervalTimer(() => {
// Coger un número aleatorio de los que quedan y borrarlo
let randomIndex = Math.floor(Math.random() * numbersLeft.length);
let randomNumber = numbersLeft[randomIndex];
if (randomIndex > -1) numbersLeft.splice(randomIndex, 1);
// Pasarle el número cogido al estado
const tempNList = numberList;
if (lastNumber) numberList.push(lastNumber);
setNumberList(tempNList);
setNumber(randomNumber);
lastNumber = randomNumber;
}, BALLTIME);
const startGame = () => {
interval.start();
};
const pauseGame = () => {
interval.pause();
};
Finally I solved it this way. I made start() return the this.timerId and then I can call pause(timerId) so basically I am taking the this.timerId outside the class to use it via parameter. Now it's working, but honestly I don't know why it wasn't working before. Thanks anyway for spending time on it!
class IntervalTimer {
callbackStartTime;
remaining = 0;
paused = false;
timerId = null;
_callback;
_delay;
constructor(callback, delay) {
this._callback = callback;
this._delay = delay;
}
pause(timerIDfromOutside) { //using the returned this.timerID in start()
if (!this.paused) {
this.clear(timerIDfromOutside);
this.remaining = new Date().getTime() - this.callbackStartTime;
this.paused = true;
}
}
resume() {
if (this.paused) {
if (this.remaining) {
setTimeout(() => {
this.run();
this.paused = false;
this.start();
}, this.remaining);
} else {
this.paused = false;
this.start();
}
}
}
clear(timerIDfromOutside) {
clearInterval(timerIDfromOutside);
}
start() {
this.paused = false
this.clear();
this.timerId = setInterval(() => {
this.run();
}, this._delay);
return this.timerId //return this.timerId so I can pass it as a parameter in
// pause()
}
run() {
this.callbackStartTime = new Date().getTime();
this._callback();
}
}

React Countdown Timer unable to make a stop function

I am practically new to React. In this App I am using Hooks!
I've made a Countdown Timer that will show in a few seconds after logging in. I am unable to make it stop on a button click. I need some advise on this as I've been struggling for the past 2 days with this.
This is my code so far: (Please Help)
function Admin() {
const [isTimerOpen, setTimmer] = useState(false);
let history = useHistory();
// SET BY THE ADMIN
var minutesToCountDown = 0.9;
// TRANSFORM INTO SECONDS
var transformMinutesToSeconds = minutesToCountDown * 60
// KEEP A STATE
const [counterValue, setCounterValue] = useState(0);
const [isTimmerStoped, setStopTimer] = useState(false);
// FUNCTION TO HAPPEN EVERY 1 SECOND
function timeIt() {
if (isTimmerStoped === false) {
transformMinutesToSeconds--
setCounterValue(transformMinutesToSeconds)
console.log("Timer is on: ", transformMinutesToSeconds)
if (transformMinutesToSeconds === 0) {
clearInterval(interval)
setStopTimer(true)
}
} else {
setStopTimer(true)
clearInterval(interval)
}
}
// STARTS THE COUNTDOWN
var interval;
const startCountdown = () => {
interval = setInterval(timeIt, 1000)
}
const stopCountdown = () => {
console.log("Stop Timer")
setStopTimer(true);
setCounterValue(0);
setTimmer(false);
}
// ADD 0 IN FRONT ON THE TIME REMAINING
const addLeadingZeros = value => {
value = String(value);
while (value.length < 2) {
value = `0${value}`;
}
return value;
};
// CONVERT SECONDS INTO TIME REMAINING
function convertSeconds(seconds) {
var min = Math.floor(seconds / 60);
var sec = seconds % 60;
return addLeadingZeros(min) + ':' + addLeadingZeros(sec)
}
const logOutUser = () => {
logout();
return history.push(mainRoute)
}
function setTimer() {
const timer = setTimeout(() => {
setTimmer(true)
console.log('This will run after 3 seconds!')
startCountdown()
}, sessionTimeout);
return () => clearTimeout(timer);
}
useEffect(() => {
if (isTimmerStoped === false) {
console.log('Effect Starting', isTimmerStoped)
setTimer()
} else {
console.log('Effect Stopping', isTimmerStoped)
stopCountdown()
}
}, [isTimmerStoped, setStopTimer, minutesToCountDown]);
return <React.Fragment>
<CssBaseline />
<Container disableGutters maxWidth={false}>
<NavigationBar handleSignOut={logOutUser}/>
<TimerContent
timeRemaining={convertSeconds(counterValue)}
isTimerAlertOpen={isTimerOpen}
extendSessionBtn={stopCountdown}
logoutBtn={logOutUser}
clickOutsideButton={stopCountdown}/>
</Container>
</React.Fragment>
}
export default Admin;
You should do 2 things.
Make the interval variable a ref. This way it's value will be unique every where it imported. Note: Just creating a variable above the component is a bad idea because that variable will be shared between each component that imports the Admin component, which will lead to bugs.
Wrong
let interval;
function Admin() {
//... code here
// STARTS THE COUNTDOWN
// var interval; Remove from here
const startCountdown = () => {
interval = setInterval(timeIt, 1000)
}
//... code here
}
export default Admin;
Right
function Admin() {
const interval = React.useRef();
//... code here
// STARTS THE COUNTDOWN
// var interval; Remove from here
const startCountdown = () => {
interval.current = setInterval(timeIt, 1000)
}
//... code here
}
export default Admin;
Add clearInterval to your stopCountdown function. You can remove the clearInterval in the timeIt function and move it into stopCountdown. Please take a look at this codepen I made to demostrate
const stopCountdown = () => {
console.log("Stop Timer")
setStopTimer(true);
setCounterValue(0);
setTimmer(false);
clearInterval(interval.current) // Clear the interval here
}
You shouldn't maintain the intervalId returned by setInterval in a functional variable since it will be reset on re-render and you will loose the actual timer Id. You must instead use useRef hook to store the timerId
const interval = useRef(null);
...
function timeIt() {
if (isTimmerStoped === false) {
transformMinutesToSeconds--
setCounterValue(transformMinutesToSeconds)
console.log("Timer is on: ", transformMinutesToSeconds)
if (transformMinutesToSeconds === 0) {
clearInterval(interval.current)
setStopTimer(true)
}
} else {
setStopTimer(true)
clearInterval(interval.current)
}
}
...
// STARTS THE COUNTDOWN
const startCountdown = () => {
interval.current = setInterval(timeIt, 1000)
}

Javascript: Detecting a long press

In order to differentiate between scroll and drag&drop on touch devices I decided to consider that drag event occurred if it follows long press.
Is there a way to make code below cleaner?
const listItem = document.getElementById("listItem");
listItem.addEventListener("touchstart", onTouchstart);
listItem.addEventListener("touchmove", onTouchmove);
listItem.addEventListener("touchend", onTouchend);
const longpress = false;
const longpressStart = 0;
const longpressChecked = false;
const LONGPRESS_DURATION = 100;
function onTouchstart() {
longpress = false;
longpressStart = Date.now();
}
function isLongPress() {
if (longpressChecked) {
return longpress;
}
if (Date.now() - longpressStart >= LONGPRESS_DURATION) {
longpress = true;
}
longpressChecked = true;
return longpress;
}
function onTouchmove() {
if (isLongPress()) {
// drag and drop logic
}
}
function onTouchend() {
longpress = false;
longpressStart = 0;
longpressChecked = false;
}
Thank you for help
You could beautify this through using some curried arrow functions:
const listen = (el, name) => handler => el.addEventListener(name, handler);
const since = (onStart, onEnd) => {
let last = 0;
onStart(() => last = Date.now());
onEnd(() => last = 0);
return time => Date.now() - last < time;
};
So you can just do:
const longPress = since(
listen(listItem, "touchstart"),
listen(listItem, "touchend")
);
listen(listItem, "touchmove")(evt => {
if(longPress(100)) {
//...
}
});
const listItem = document.getElementById("listItem");
listItem.addEventListener("touchstart", onTouchstart);
listItem.addEventListener("touchmove", onTouchmove);
listItem.addEventListener("touchend", onTouchend);
var onlongtouch = false;
function onTouchstart() {
timer = setTimeout(function() {
onlongtouch = true;
}, 100);
}
function onTouchmove() {
if(onlongtouch) {
// do something
}
}
function onTouchend() {
if (timer)
clearTimeout(timer);
onlongtouch = false;
}

Wait until a condition is true?

I'm using navigator.geolocation.watchPosition in JavaScript, and I want a way to deal with the possibility that the user might submit a form relying on location before watchPosition has found its location.
Ideally the user would see a 'Waiting for location' message periodically until the location was obtained, then the form would submit.
However, I'm not sure how to implement this in JavaScript given its lack of a wait function.
Current code:
var current_latlng = null;
function gpsSuccess(pos){
//console.log('gpsSuccess');
if (pos.coords) {
lat = pos.coords.latitude;
lng = pos.coords.longitude;
}
else {
lat = pos.latitude;
lng = pos.longitude;
}
current_latlng = new google.maps.LatLng(lat, lng);
}
watchId = navigator.geolocation.watchPosition(gpsSuccess,
gpsFail, {timeout:5000, maximumAge: 300000});
$('#route-form').submit(function(event) {
// User submits form, we need their location...
while(current_location==null) {
toastMessage('Waiting for your location...');
wait(500); // What should I use instead?
}
// Continue with location found...
});
Modern solution using Promise
function waitFor(conditionFunction) {
const poll = resolve => {
if(conditionFunction()) resolve();
else setTimeout(_ => poll(resolve), 400);
}
return new Promise(poll);
}
Usage
waitFor(_ => flag === true)
.then(_ => console.log('the wait is over!'));
or
async function demo() {
await waitFor(_ => flag === true);
console.log('the wait is over!');
}
References
Promises
Arrow Functions
Async/Await
Personally, I use a waitfor() function which encapsulates a setTimeout():
//**********************************************************************
// function waitfor - Wait until a condition is met
//
// Needed parameters:
// test: function that returns a value
// expectedValue: the value of the test function we are waiting for
// msec: delay between the calls to test
// callback: function to execute when the condition is met
// Parameters for debugging:
// count: used to count the loops
// source: a string to specify an ID, a message, etc
//**********************************************************************
function waitfor(test, expectedValue, msec, count, source, callback) {
// Check if condition met. If not, re-check later (msec).
while (test() !== expectedValue) {
count++;
setTimeout(function() {
waitfor(test, expectedValue, msec, count, source, callback);
}, msec);
return;
}
// Condition finally met. callback() can be executed.
console.log(source + ': ' + test() + ', expected: ' + expectedValue + ', ' + count + ' loops.');
callback();
}
I use my waitfor() function in the following way:
var _TIMEOUT = 50; // waitfor test rate [msec]
var bBusy = true; // Busy flag (will be changed somewhere else in the code)
...
// Test a flag
function _isBusy() {
return bBusy;
}
...
// Wait until idle (busy must be false)
waitfor(_isBusy, false, _TIMEOUT, 0, 'play->busy false', function() {
alert('The show can resume !');
});
This is precisely what promises were invented and implemented (since OP asked his question) for.
See all of the various implementations, eg promisejs.org
You'll want to use setTimeout:
function checkAndSubmit(form) {
var location = getLocation();
if (!location) {
setTimeout(checkAndSubmit, 500, form); // setTimeout(func, timeMS, params...)
} else {
// Set location on form here if it isn't in getLocation()
form.submit();
}
}
... where getLocation looks up your location.
You could use a timeout to try to re-submit the form:
$('#route-form').submit(function(event) {
// User submits form, we need their location...
if(current_location==null) {
toastMessage('Waiting for your location...');
setTimeout(function(){ $('#route-form').submit(); }, 500); // Try to submit form after timeout
return false;
} else {
// Continue with location found...
}
});
export default (condition: Function, interval = 1000) =>
new Promise((resolve) => {
const runner = () => {
const timeout = setTimeout(runner, interval);
if (condition()) {
clearTimeout(timeout);
resolve(undefined);
return;
}
};
runner();
});
class App extends React.Component {
componentDidMount() {
this.processToken();
}
processToken = () => {
try {
const params = querySearch(this.props.location.search);
if('accessToken' in params){
this.setOrderContext(params);
this.props.history.push(`/myinfo`);
}
} catch(ex) {
console.log(ex);
}
}
setOrderContext (params){
//this action calls a reducer and put the token in session storage
this.props.userActions.processUserToken({data: {accessToken:params.accessToken}});
}
render() {
return (
<Switch>
//myinfo component needs accessToken to retrieve my info
<Route path="/myInfo" component={InofUI.App} />
</Switch>
);
}
And then inside InofUI.App
componentDidMount() {
this.retrieveMyInfo();
}
retrieveMyInfo = async () => {
await this.untilTokenIsSet();
const { location, history } = this.props;
this.props.processUser(location, history);
}
untilTokenIsSet= () => {
const poll = (resolve) => {
const { user } = this.props;
const { accessToken } = user;
console.log('getting accessToken', accessToken);
if (accessToken) {
resolve();
} else {
console.log('wating for token .. ');
setTimeout(() => poll(resolve), 100);
}
};
return new Promise(poll);
}
Try using setInterval and clearInterval like this...
var current_latlng = null;
function gpsSuccess(pos) {
//console.log('gpsSuccess');
if (pos.coords) {
lat = pos.coords.latitude;
lng = pos.coords.longitude;
} else {
lat = pos.latitude;
lng = pos.longitude;
}
current_latlng = new google.maps.LatLng(lat, lng);
}
watchId = navigator.geolocation.watchPosition(gpsSuccess,
gpsFail, {
timeout: 5000,
maximumAge: 300000
});
$('#route-form').submit(function (event) {
// User submits form, we need their location...
// Checks status every half-second
var watch = setInterval(task, 500)
function task() {
if (current_latlng != null) {
clearInterval(watch)
watch = false
return callback()
} else {
toastMessage('Waiting for your location...');
}
}
function callback() {
// Continue on with location found...
}
});
This accepts any function, even if it's async, and when it evaluates to a truthy value (checking every quarter-second by default), resolves to it.
function waitFor(condition, step = 250, timeout = Infinity) {
return new Promise((resolve, reject) => {
const now = Date.now();
let running = false;
const interval = setInterval(async () => {
if (running) return;
running = true;
const result = await condition();
if (result) {
clearInterval(interval);
resolve(result);
} else if (Date.now() - now >= timeout * 1000) {
clearInterval(interval);
reject(result);
}
running = false;
}, step);
});
}
Example
example();
async function example() {
let foo = 'bar';
setTimeout(() => foo = null, 2000);
console.log(`foo === "${foo}"`);
await waitFor(() => foo === null);
console.log('2 seconds have elapsed.');
console.log(`foo === ${foo}`);
}
function waitFor(condition, step = 250, timeout = Infinity) {
return new Promise((resolve, reject) => {
const now = Date.now();
let running = false;
const interval = setInterval(async () => {
if (running) return;
running = true;
const result = await condition();
if (result) {
clearInterval(interval);
resolve(result);
} else if (Date.now() - now >= timeout * 1000) {
clearInterval(interval);
reject(result);
}
running = false;
}, step);
});
}

Categories