onended() does not fire in Safari or on iOS - javascript

The following code works on Chrome 80.0 and Firefox 74.0 (OSX 10.14.6). However, on OSX Safari 13.0.5 or on iOS (testing in Chrome, Safari), the <div> element never turns blue, indicating that the onended callback does not fire. Is this a bug?
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const buff = ctx.createBuffer(1, 32, ctx.sampleRate);
const buffSource = ctx.createBufferSource();
buffSource.buffer = buff;
buffSource.loop = false;
// attempt to add an event listener to the buffer source
buffSource.addEventListener('ended', () => {
document.getElementById('test').style.backgroundColor = 'blue';
});
// another slight variation using the named function
function changeBackground() {
document.getElementById('test').style.backgroundColor = 'blue';
}
buffSource.addEventListener('ended', changeBackground);
// the documentation suggestion modifying the function directly
buffSource.onended = function(){
document.getElementById('test').style.backgroundColor = 'blue';
};
// maybe binding would help?
buffSource.onended = function(){
document.getElementById('test').style.backgroundColor = 'blue';
}.bind(this);
document.getElementById('button').onclick = (e) => {
ctx.resume();
buffSource.start();
document.getElementById('message').innerText = "Pressed Button";
};
#test {
background-color: red;
width: 500px;
height: 500px;
}
#button {
cursor: pointer;
}
<!DOCTYPE html>
<html lang="en">
<body>
<button id = 'button'>Click me</button>
<div id = 'test'></div>
<div id = 'message'></div>
</body>
</html>

Safari doesn't fire the ended event if the AudioBufferSourceNode is not connected.
buffSource.connect(ctx.destination);
Executing this line before calling start() should make it work.

Related

JavaScript how to get if a button is active (after click)

I have only one button. In css you can use button:active { do stuff } and it will become valid once and after the button is clicked, so interacting with other objects (clicking on a image) will cause
the statement to be null. How Can I translate this into java script?
Something like that:
const Ham_Button = document.querySelector('button');
while (Ham_Button.isActive)
{
do stuff
}
I tried this:
const Ham_Button = document.querySelector('button');
const ClickEvent = function() {
Hidden_Nav.style.display = "block";
}
Ham_Button.addEventListener("click", ClickEvent);
But the event is triggered only and only when I click, not after, when the element is still the last interacted object.
Maybe you can use the onblur event. With that you can detect if the user "removes" the active state of a link or button.
https://developer.mozilla.org/en-US/docs/Web/API/Element/blur_event
You can find out which element currently has focus by consulting
document.activeElement
This has good browser compatibility.
Don't put this in a while loop as in your example, though, or you'll lock up the browser's thread of execution. If you must check this periodically, consider using setTimeout.
Maybe you could try something like this:
const Ham_Button = document.querySelector('button');
let hamIsActive = false;
Ham_button.addEventListener('click', () => {
if(hamIsActive) {
// do stuff, add styles...
hamIsActive = false; // to add a toggle functionality
} else {
// do stuff, add styles...
hamIsActive = true; // to add a toggle functionality
}
})
You have the mousedown event fired once each time the button is pressed.
You also have the mouseup event fired once each time the button is released.
you can't use a while loop since it would block the main thread but you can setup a tick loop (fired roughly every 16ms, or 60 times per second)
const btn = document.getElementById('btn');
const counterDown = document.getElementById('counter_down');
let totalTimeDown = 0;
let downRequestId;
let lastTimeDown = 0;
function loopDown(time) {
if (lastTimeDown) {
const dt = time - lastTimeDown;
totalTimeDown += dt;
counterDown.innerHTML = (totalTimeDown / 1000).toFixed(2);
}
lastTimeDown = time;
downRequestId = requestAnimationFrame(loopDown);
}
function onMouseDown() {
downRequestId = requestAnimationFrame(loopDown);
}
function onMouseUp() {
lastTimeDown = 0;
cancelAnimationFrame(downRequestId);
}
btn.addEventListener('mousedown', onMouseDown);
btn.addEventListener('mouseup', onMouseUp);
// SAME LOGIC FOR FOCUS/BLUR
const counterFocus = document.getElementById('counter_focus');
let totalTimeFocused = 0;
let focusRequestId;
let lastTimeFocus = 0;
function loopFocus(time) {
if (lastTimeFocus) {
const dt = time - lastTimeFocus;
totalTimeFocused += dt;
counterFocus.innerHTML = (totalTimeFocused / 1000).toFixed(2);
}
lastTimeFocus = time;
focusRequestId = requestAnimationFrame(loopFocus);
}
function onFocus() {
focusRequestId = requestAnimationFrame(loopFocus);
}
function onBlur() {
lastTimeFocus = 0;
cancelAnimationFrame(focusRequestId);
}
btn.addEventListener('focus', onFocus);
btn.addEventListener('blur', onBlur);
button:focus {
outline: 1px solid blue;
}
button:active {
outline: 1px solid red;
}
<button id="btn">press me</button>
<div>time pressed: <span id="counter_down">0</span>s</div>
<div>time focused: <span id="counter_focus">0</span>s</div>
Note that you can switch for focus and blur events if that's what you are looking for

speechSynthesis.pause() should return true but returning false

When I play and pause and play again it does not play because
speechSynthesis.pause() does not make speechSynthesis.paused true which should make it true.
I've checked the docs it says
The paused read-only property of the SpeechSynthesis interface is a Boolean that returns true if the SpeechSynthesis object is in a paused state, or false if not.
meaning the line speechSynthesis.pause() didnt set it to true
Sample below:
const playButton = document.getElementById("play-button");
const pauseButton = document.getElementById("pause-button");
const stopButton = document.getElementById("stop-button");
const textInput = document.getElementById("text");
const speedInput = document.getElementById("speed");
const status = document.getElementById("status");
let currentCharacter;
const utterance = new SpeechSynthesisUtterance();
utterance.addEventListener("end", () => {
textInput.disabled = false;
})
utterance.addEventListener("bounary", e => {
currentCharacter = e.charIndex;
})
playButton.addEventListener("click", () => {
playText(textInput.value);
})
pauseButton.addEventListener("click", pauseText);
stopButton.addEventListener("click", stopText);
speedInput.addEventListener("input", () => {
stopText();
playText(utterance.text.substring(currentCharacter))
})
function playText(text) {
if (speechSynthesis.paused && speechSynthesis.speaking) {
status.innerHTML = "resumed";
return speechSynthesis.resume()
}
status.innerHTML = "played";
if (speechSynthesis.speaking) return
utterance.text = text;
utterance.rate = speedInput.value || 1;
textInput.disabled = true;
speechSynthesis.speak(utterance);
}
function pauseText() {
if (speechSynthesis.speaking) {
status.innerHTML = "paused";
speechSynthesis.pause() //
}
}
function stopText() {
status.innerHTML = "stopped";
speechSynthesis.resume()
speechSynthesis.cancel()
}
body {
width: 90%;
margin: 0 auto;
margin-top: 1rem;
}
#text {
width: 100%;
height: 50vh;
}
<textarea name="text" id="text"></textarea>
<label for="speed">Speed</label>
<input type="number" name="speed" id="speed" min=".5" max="3" step=".5" value="1">
<button id="play-button">Play</button>
<button id="pause-button">Pause</button>
<button id="stop-button">Stop</button>
<span id="status"></span>
You can use this code to see the statuses upon clicking Play, Pause and Stop. The statues are then updated when the relevent event is triggered. I.e. when you click Play the statuses are shown once upon clicking and again upon the onstart event being fired.
You'll see that there is a difference between these two for example when you click Pause the paused flag is false until the onpause event is triggered a moment later.
Given that chrome has problems and there are timing issues I suggest that you keep it simple and have start, pause, resume and cancel buttons with their own functions.
const showStatus = (btn) => {
const statuses = document.createTextNode(`
${btn}:
Pending: ${speechSynthesis.pending},
Speaking: ${speechSynthesis.speaking},
Paused: ${speechSynthesis.paused}`);
const p = document.createElement('p');
p.appendChild(statuses);
data.insertBefore(p, data.firstElementChild);
};
play.addEventListener("click", (e) => {
const u = new SpeechSynthesisUtterance(
"1 2 3 4 5 6 7 8 9 10 11 12 13 14 15"
);
speechSynthesis.speak(u);
showStatus(e.target.innerText);
u.onstart = () => showStatus('On start');
u.onpause = () => showStatus('On pause');
u.onresume = () => showStatus('On resume');
u.onend = () => showStatus('On end');
u.onerror = (err) => showStatus('On error ' + err);
});
pause.addEventListener("click", (e) => {
speechSynthesis.pause();
showStatus(e.target.innerText);
});
resume.addEventListener("click", (e) => {
speechSynthesis.resume();
showStatus(e.target.innerText);
});
ssCancel.addEventListener('click', (e) => {
speechSynthesis.cancel();
showStatus(e.target.innerText);
});
showStatus('Upon load');
<button id="play">Play</button>
<button id="pause">Pause</button>
<button id="resume">Resume</button>
<button id="ssCancel">Cancel</button>
<div id="data"></div>
I had the same problem. My reading of the docs did not coincide with what I experienced. I ended up simply setting my own variable to keep track of whether there is a pause. Of note, the speaking property can be used to find if the utterance has not been started, or has finished. That coupled with my own variable keeping track of what to do when the user presses the pause button allowed me to get a stop, play and pause feature.
Edit: This works for me in Edge Browser. I plan to update after I try in others.
const synth = window.speechSynthesis;
let myPauseProperty = false;
let utterThis;
function stop() {
myPauseProperty = false;
synth.cancel(utterThis);
}
function play() {
if ((myPauseProperty === true)&&(synth.speaking===true)) {//alredy started not ended
//speech somewhere in the middle, and paused, needs resumed
synth.resume();
myPauseProperty=false;
}
else if (synth.speaking===false) {
//not started or has ended, user expects to start play
utterThis = new SpeechSynthesisUtterance(document.getElementById("text-area").value);
synth.speak(utterThis);
myPauseProperty=false;
}
}
function pause() {
if ((myPauseProperty === true)&&(synth.speaking===true)) {
//speech somewhere in middle of phrase and paused, user wants unpause
synth.resume();
myPauseProperty = false;
}
else if ((myPauseProperty === false)&&(synth.speaking===true)) {
//speech somewhere in middle of phrase and unpaused, user wants pause
synth.pause()
myPauseProperty=true;
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<textarea id="text-area">Put your text here and try playing.</textarea>
<nav>
<button onclick="stop();">[]</button>
<button onclick="play();">|></button>
<button onclick="pause();">||</button>
</nav>
</body>
</html>

Javascript - use callback with addEventListener for "remove all" button

I'm adding a "remove all" button to my chrome extension which as the name suggests should serve to remove all saved links.
The button works only after I have performed some other action first (adding a link, removing a link etc) but of course, I want it to work right away.
I have read that this may be due to the code being asynchronous so I tried introducing a callback function with the help of this question: Add callback to .addEventListener however, it's still performing the same way it did before so maybe that wasn't the issue after all, or I may have read the other question wrong. I appreciate any tips so thank you in advance. I will try to figure it out myself in the meantime.
var urlList = [];
var i = 0;
document.addEventListener('DOMContentLoaded', function() {
getUrlListAndRestoreInDom();
// event listener for the button inside popup window
document.getElementById('save').addEventListener('click', addLink);
});
function addLink() {
var url = document.getElementById("saveLink").value;
addUrlToListAndSave(url);
addUrlToDom(url);
}
function getUrlListAndRestoreInDom() {
chrome.storage.local.get({
urlList: []
}, function(data) {
urlList = data.urlList;
urlList.forEach(function(url) {
addUrlToDom(url);
});
});
}
function addUrlToDom(url) {
// change the text message
document.getElementById("saved-pages").innerHTML = "<h2>Saved pages</h2>";
var newEntry = document.createElement('li');
var newLink = document.createElement('a');
var removeButton = document.createElement('button');
removeButton.textContent = "Remove";
removeButton.type = "button";
removeButton.className = "remove";
removeButton.addEventListener("click", function() {
var anchor = this.previousElementSibling;
var url = anchor.getAttribute("href");
removeUrlAndSave(url);
this.parentNode.remove();
});
newLink.textContent = url;
newLink.setAttribute('href', url);
newLink.setAttribute('target', '_blank');
newEntry.appendChild(newLink);
newEntry.appendChild(removeButton);
newEntry.className = "listItem";
document.getElementById("list").appendChild(newEntry);
}
function removeUrlAndSave(url) {
var index = urlList.indexOf(url);
if (index != -1) {
urlList.splice(index, 1);
saveUrlList();
}
}
function addUrlToListAndSave(url) {
urlList.push(url);
saveUrlList();
//}
}
function saveUrlList(callback) {
chrome.storage.local.set({
urlList
// }, function() {
// if (typeof callback === 'function') {
// //If there was no callback provided, don't try to call it.
// callback();
// }
// });
});
function removeMe(i) {
var fullList = documents.getElementsByClassName('listItem');
listItem[i].parentNode.removeChild(listItem[i]);
}
function removeAll() {
var removeList = document.getElementsByClassName("listItem");
while (removeList[0]) {
removeList[0].parentNode.removeChild(removeList[0]);
};
}
function registerElement(callback) {
var element = document.getElementById("remove-all-button");
element.addEventListener("click", callback);
}
registerElement(removeAll);
#list {
min-height: 360px;
max-height: 360px;
width: 300px;
margin: auto;
overflow: scroll;
}
<head>
<script type="text/javascript" src="popup.js"></script>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div>
<span id="hot-button">Hot Drops</span>
<li id="removeAll">
<button id="remove-all-button"> Remove All</button>
</li>
<span id="saved-pages"></span>
<div>
<ul id="list"></ul>
</div>
</div>
<input type="button" id="save" value="Add">
<input type="text" id="saveLink" name="member" value=""><br/>
<span id="settings-button">Settings</span>
</body>
</html>
there you go
jsfiddle
had to write this line to post link
http://jsfiddle.net/1w69bo8k/ - you made a lot of mistakes, where you put certain code, to using variables I never saw them defined anywhere in code
(you keep editing the question, I went from this fiddle you provided)
change back to your getUrlListAndRestoreInDom function, deleted the storage code
you update the storage and the urlList manually, rather do it after the update of local is successful

How to sync media perfectly in HTML?

Let's say I would like to synchronize two videos and avoid echos. I meant that the two tracks should stay in sync as though playing a regular video file with audio.
<video id="A" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls=""></video><br>
<p>Current Time:<span id="aTime"></span></p><br>
<video id="B" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls=""></video><br>
<p>Current Time:<span id="bTime"></span></p><br>
<button onclick="playM()">Play</button>
<button onclick="pauseM()">Pause</button>
<script>
var a = document.getElementById("A");
var b = document.getElementById("B");
function playM() {
a.play();
b.play();
}
function pauseM() {
a.pause();
b.pause();
}
a.ontimeupdate = function() {
document.getElementById("aTime").innerHTML = a.currentTime;
};
b.ontimeupdate = function() {
document.getElementById("bTime").innerHTML = b.currentTime;
};
a.onseeked = function() {
b.currentTime= a.currentTime;
};
b.onseeked = function() {
a.currentTime = b.currentTime;
};
</script>
Update
After some experimentation, I've discovered that the an offset of mere microseconds is great, but not really necessary for syncing videos. In Demo 2 we have video tag A and B. A has 4 eventListener() and video B as the callback. play(), pause(), and seek are synced between A and B with A being the "master" and B the "slave".
Demo 2
<!DOCTYPE html>
<html>
<head>
<style>
html {
font: 400 16px/1.5 Consolas
}
section {
display: flex;
justify-content: space-around;
width: 100%;
height: calc(100% - 160px);
}
button {
padding: 0;
border: 0;
}
</style>
</head>
<body>
<section>
<video id="A" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls width='240' height='135'></video>
<video id="B" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls width='240' height='135'></video>
</section>
<script>
var A = document.getElementById("A");
var B = document.getElementById("B");
A.addEventListener("play", function() {
B.play();
});
A.addEventListener("pause", function() {
B.pause();
});
A.addEventListener("seeking", function() {
B.currentTime = A.currentTime;
});
A.addEventListener("seeked", function() {
B.currentTime = A.currentTime;
});
</script>
</body>
</html>
JavaScript is synchronous, meaning that it processes one thing at a time. Syncing media is possible by using Promises which allows asynchronous operations but it's syntax is unintuitive and down right difficult. Fortunately async and await just got released in all major browsers* only a couple of months ago. All of the behavior and flow of Promises are in await. Here's the basic syntax involved using 2 functions asynchronously:
async function functionName() {
var A = await functionA();
var B = await functionB();
var C = A + B;
return C;
}
Details are commented in Demo
References are at the bottom of post
Demo
/* For details on how this demo controls the <form>,
|| see HTMLFormControlsCollection in References
*/
var x = document.forms.ui.elements;
var playM = x.playM;
var pauseM = x.pauseM;
/* Register the buttons to the click event
|| callback is asyncCall()
|| Register a video tag to the ended event
|| callback is just an anonymous function to
|| display 'IDLE' message when the videos have ended
*/
/* Avoid the use of on event handlers, use event
|| listeners instead. For details on Event Listeners
|| see addEventListener() in References
*/
playM.addEventListener('click', asyncCall, false);
pauseM.addEventListener('click', asyncCall, false);
document.querySelector('video').addEventListener('ended', function(e) {
x.display.value = 'IDLE';
}, false);
/* This callback is an Async Function which uses a key
|| word called "await". await waits for a function
|| to start then moves on to the next await to
|| see when it's ready to start. The players are
|| playing asynchronously (i.e. congruently, i.e i.e. in
|| parallel). Normally JavaScript is synchronous because
|| the evaluation and execution of a function is the
|| only thing a browser would do, so everything else
|| had to wait their turn. That is called: "blocking".
|| While await is waiting, the browser is allowed to do
|| other processing. For details on async and await,
|| see References.
*/
async function asyncCall(e) {
var A = document.getElementById("A");
var B = document.getElementById("B");
var status;
/* if/if else condition to determin which button was
|| actually clicked. Use e.target to get the origin
|| of event (i.e. clicked button). For details on
|| Event.target, see References
*/
// if the button's #id is 'playM'...
if (e.target.id === 'playM') {
var mediaA = await mPlay(A);
x.outA.value = performance.now(mediaA);
var mediaB = await mPlay(B);
x.outB.value = performance.now(mediaB);
status = mPlay(B);
// otherwise if the button's #id is 'pauseM'...
} else if (e.target.id === 'pauseM') {
var mediaA = await mIdle(A);
x.outA.value = performance.now(mediaA);
var mediaB = await mIdle(B);
x.outB.value = performance.now(mediaB);
status = mIdle(B);
} else {
status = 'IDLE';
}
x.display.value = status;
return status;
}
// Simple function used in asyncCall() to play
function mPlay(ele) {
var state = 'PLAYING';
ele.play();
return state;
}
// Simple function used in asyncCall() to pause
function mIdle(ele) {
var state = 'IDLE';
ele.pause();
return state;
}
html {
font: 400 16px/1.5 Consolas
}
section {
display: flex;
justify-content: space-around;
width: 100%;
height: calc(100% - 160px);
}
button {
padding: 0;
border: 0;
}
#playM:before {
content: '▶';
font-size: 32px
}
#pauseM::before {
content: '⏸';
font-size: 32px;
}
<section>
<video id="A" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls width='160' height='90'></video>
<video id="B" src="http://media.w3.org/2010/05/sintel/trailer.webm" controls width='160' height='90'></video>
</section>
<form id='ui'>
<fieldset>
<legend>Asynchronous Playback</legend>
<button id='playM' type='button'></button>
<button id='pauseM' type='button'></button>
<output id='display'></output><br><br>
<label for='outA'>Source A: </label>
<output id='outA'></output><br><br>
<label for='outB'>Source B: </label>
<output id='outB'></output><br><br>
</fieldset>
</form>
References
async function() {... await
HTMLFormControlsCollection
Event.target
Event.target .addEventListener( 'event', function, false )
You can use plain JavaScript for this. A good starting point and reference would be W3C Schools. The currentTime property with an oncuechange function should get you started in checking changes in the timeline and seeking.
Although, I don't understand your restrictions (if any) so don't know why you wouldn't just use a video file with the audio and video already synced.

Disable selection context menu in iOS safari

I want to disable the default context menu that appears after a certain text is selected in iOS Safari (web browser). Is that possible?
The only way i found was by removing the selection and select again with javascript.
Have a look at my code:
/* prevent ios edit-menu */
if (/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
!function(){
var target = document.body; // the element where the edit menue should be disabled
var preventSelRecursion;
document.addEventListener('selectionchange', function(e){
var S = getSelection();
if (!S.rangeCount) return;
if (S.isCollapsed) return;
var r = S.getRangeAt(0);
if (!target.contains(r.commonAncestorContainer)) return;
if (preventSelRecursion) return;
iosSelMenuPrevent();
}, false);
var iosSelMenuPrevent = debounce(function(){
var S = getSelection();
var r = S.getRangeAt(0);
preventSelRecursion = true;
S = getSelection();
S.removeAllRanges();
setTimeout(function(){ // make remove-add-selection removes the menu
S.addRange(r);
setTimeout(function(){
preventSelRecursion = false;
});
},4);
},800); // if no selectionchange during 800ms : remove the menu
/* helper-function */
function debounce(fn, delay) {
var timer = null;
return function () {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
}();
}
It is possible, see this example. Basically, the important part is to set the right css atributes:
body { -webkit-touch-callout: none !important; }
a { -webkit-user-select: none !important; }
Also, here is a question which solves a similar issue
According to onclick blocks copy+paste on Mobile Safari?, if the text is in an element that has an onclick event, the context menu won't be displayed.
Inspired by Hans Gustavson's answer, I propose a simpler solution in TypeScript:
function disableIosSafariCallout(this: Window, event: any) {
const s = this.getSelection();
if ((s?.rangeCount || 0) > 0) {
const r = s?.getRangeAt(0);
s?.removeAllRanges();
setTimeout(() => {
s?.addRange(r!);
}, 50);
}
}
document.ontouchend = disableIosSafariCallout.bind(window);
This solution is actually a workaround. When you select a text, you might still see the text selection callout shows and then disappear immediately. I am not sure whether Hans Gustavson's answer has the same defect...

Categories

Resources