WebRTC Audio/Video not syncing - audio delay - google-chrome

Background:
I'm working on a Django project using webRTC for video/audio recording for a user registration.
I used this repository (demo) as a base for my code, then added my features.
Here the workflow with all my changes:
The user click on a button to start the stream.
The stream stops after 10 sec. While recording, the user can see a countdown timer when it has 5 seconds left.
When a video has been registered, I change the recorder (#gum) to the recorded one (#recorded) so the user can play it, pause it etc.
If the user is not satisfied, he can click on the .start-recording button that now has a text content saying "Record new video".
If the user is satisfied and has filled up the rest of the form, on the form submit I use the blob in the registration-video input to save the file.
Issue:
Using Firefox 70.0.1 (64 bits): on step 3 Video/Audio sync is perfect.
Using Chrome 84.0.4147.105 (64 bits) : on step 3 Audio sync is delayed for about 3 seconds.
Requests:
Did I do something wrong?
Is there a trick to fix that please?
My code below :
HTML Code:
<div class="video-div">
<h2>Etape 1</h2>
<div class="video-content">
<video id="gum" class="video-show" poster="{% static 'core/images/video-show-poster.png' %}" playsinline autoplay muted></video>
<video id="recorded" class="video-hide" playsinline></video>
<p class="timer"></p>
</div>
<button class="btn start-recording" id="record">Enregistrer ma vidéo</button>
</div>
<div class="echo-cancellation">
<h4>Media Stream Constraints options</h4>
<p>Echo cancellation: <input type="checkbox" id="echoCancellation"></p>
</div>
<div>
<span id="errorMsg"></span>
</div>
<form id="registration-form" action="" method="POST" enctype="multipart/form-data" novalidate>
{% csrf_token %}
{% for field in form_registration %}
{% if field.name != "registration_video" %}
<div class="form-group">
<div> {{ field.label_tag }} {{ field }}</div>
</div>
{% endif %}
{% endfor %}
{% for field in form_location %}
<div class="form-group">
<div> {{ field.label_tag }} {{ field }}</div>
</div>
{% endfor %}
<input type="submit" value="Inscription" class="register-submit-btn btn">
</form>
JS Code:
<!-- Script to register user media. Adaptated to current app thanks to this resource: https://github.com/webrtc/samples/tree/gh-pages/src/content/getusermedia/record -->
<script>
let mediaRecorder;
let recordedBlobs;
const errorMsgElement = document.querySelector('span#errorMsg');
const recordedVideo = document.querySelector('video#recorded');
const recordButton = document.querySelector('button#record');
recordButton.addEventListener('click', async () => {
const hasEchoCancellation = document.querySelector('#echoCancellation').checked;
const constraints = {
audio: {
echoCancellation: {exact: hasEchoCancellation}
},
video: {
width: 290, height: 240
}
};
console.log('Using media constraints:', constraints);
await init(constraints);
//Hiding the recorded video if user record a new one, show the gum video (livecam) instead.
if(document.getElementById('gum').hasAttribute('class', 'video-hide')){
document.getElementById('gum').classList.remove('video-hide');
document.getElementById('gum').classList.add('video-show');
document.getElementById('recorded').classList.remove('video-show');
document.getElementById('recorded').classList.add('video-hide');
}
startRecording();
});
//Make all necessary action for the video recording
function startRecording()
{
recordedBlobs = [];
let options = {mimeType: 'video/webm;codecs=vp9,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: 'video/webm;codecs=vp8,opus'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: 'video/webm'};
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
//console.error(`${options.mimeType} is not supported`);
options = {mimeType: ''};
}
}
}
try {
mediaRecorder = new MediaRecorder(window.stream, options);
} catch (e) {
console.error('Exception while creating MediaRecorder:', e);
errorMsgElement.innerHTML = `Exception while creating MediaRecorder: ${JSON.stringify(e)}`;
return;
}
console.log('Created MediaRecorder', mediaRecorder, 'with options', options);
recordButton.disabled = true;
// When the recording is done we're allowing the user to register a new video
// We're using the recordedBlob as a source to play the recordedVideo automatically
mediaRecorder.onstop = (event) => {
console.log('Recorder stopped: ', event);
console.log('Recorded Blobs: ', recordedBlobs);
document.getElementById('record').textContent = 'Nouvelle tentative';
const superBuffer = new Blob(recordedBlobs, {type: 'video/webm'});
recordedVideo.src = null;
recordedVideo.srcObject = null;
recordedVideo.src = window.URL.createObjectURL(superBuffer);
recordedVideo.controls = true;
};
mediaRecorder.ondataavailable = handleDataAvailable;
mediaRecorder.start();
console.log('MediaRecorder started', mediaRecorder);
//The video should last less that 10 seconds.
//We put a timeout so the video will last less than 10sec. And an interval to display a countdown 5 secondes before the end.
let timerId = setInterval(countDown, 1000);
let i = 10;
function countDown()
{
if(i <= 5 && i > 0 ) {
document.querySelector('.timer').style.display = 'block';
document.querySelector('.timer').textContent = i + ' secondes';
} else if (i < 1){
clearInterval(timerId);
document.querySelector('.timer').textContent = '';
document.querySelector('.timer').style.display = 'none';
}
i--;
}
//When the 10 seconds are done, we're showing the user the recorded video.
setTimeout(function()
{
stopRecording();
recordButton.disabled = false;
document.getElementById('gum').classList.remove('video-show');
document.getElementById('gum').classList.add('video-hide');
document.getElementById('recorded').classList.remove('video-hide');
document.getElementById('recorded').classList.add('video-show');
stream.getTracks().forEach(
function(track)
{
track.stop();
}
);
}, 10000);
}
// If there is data, we push it to the recordedBlob array
function handleDataAvailable(event)
{
console.log('handleDataAvailable', event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
}
// We're listening to the click on submit button in order add the recordedBlob to the registration_video input thanks to the FormData class.
// Then we're sending the request after creating it with the XMLHttpRequest class.
const form = document.querySelector('form');
form.addEventListener('submit', (evt) => {
let myForm = document.querySelector('form');
let formData = new FormData(myForm);
if(recordedBlobs){
formData.append('registration_video', recordedBlobs[0], encodeURIComponent("nomaleatoire.webm"));
} else {
formData.append('registration_video', '');
}
//Handling form errors
evt.preventDefault();
const request = new XMLHttpRequest();
request.open("POST", "/register", true);
request.onload = function() {
response = JSON.parse(this.response);
if ( response.success == true) {
evt.target.querySelectorAll('input[type="text"], input[type="date"], input[type="number"], input[type="email"], textarea').forEach(
function(inputElem) {
inputElem.value = '';
});
if(document.querySelectorAll('.error-div')){
document.querySelectorAll('.error-div').forEach(
function(errorDiv)
{
errorDiv.remove();
}
)
}
window.location.href = "/";
} else {
errors = Object.assign(JSON.parse(response.errors_location), JSON.parse(response.errors_registration));
if(document.querySelectorAll('.error-div'))
{
document.querySelectorAll('.error-div').forEach(
function(errorDiv)
{
errorDiv.remove();
}
)
}
for (const [key, value] of Object.entries(errors)) {
if(key != 'registration_video') {
div = document.createElement("div")
div.setAttribute('id', 'error-div-' + key);
div.classList.add("error-div")
errorMessage = "";
value.forEach(errorfield => {
errorMessage = errorMessage + " " + errorfield.message
});
div.textContent = errorMessage
inputElem = document.querySelector('#id_' + key)
theform = inputElem.parentElement.parentElement.parentElement;
theform.insertBefore(div,inputElem.parentElement.parentElement)
} else {
//Il faut afficher une erreur pour la vidéo manquante!
errorVideoMessage = document.createElement('div');
errorVideoMessage.textContent = "L'enregistrement de la vidéo est obligatoire.";
errorVideoMessage.classList.add('error-div', 'mt-2');
document.querySelector('.start-recording').before(errorVideoMessage);
}
}
}
};
request.onerror = function() {
// request failed
};
request.send(formData);
});
function stopRecording()
{
mediaRecorder.stop();
}
function handleSuccess(stream)
{
recordButton.disabled = false;
console.log('getUserMedia() got stream:', stream);
window.stream = stream;
const gumVideo = document.querySelector('video#gum');
gumVideo.srcObject = stream;
}
async function init(constraints)
{
try {
const stream = await navigator.mediaDevices.getUserMedia(constraints);
handleSuccess(stream);
} catch (e) {
console.error('navigator.getUserMedia error:', e);
errorMsgElement.innerHTML = `navigator.getUserMedia error:${e.toString()}`;
}
}
</script>

Related

Instascan select camara to scan qr

My website reads QR CODES using the mobile phone. I have used that library
https://github.com/schmich/instascan
<div class="select">
<label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
<video autoplay muted playsinline></video>
<script async src="js/main.js"></script>
Because the mobile phone has many camaras I want to let the user to select which camara wants for scanning the qr code.
main.js:
https://simpl.info/getusermedia/sources/
https://github.com/samdutton/simpl/blob/gh-pages/getusermedia/sources/js/main.js
'use strict';
var videoElement = document.querySelector('video');
var videoSelect = document.querySelector('select#videoSource');
videoSelect.onchange = getStream;
getStream().then(getDevices).then(gotDevices);
function getDevices() {
// AFAICT in Safari this only gets default devices until gUM is called :/
return navigator.mediaDevices.enumerateDevices();
}
function gotDevices(deviceInfos) {
window.deviceInfos = deviceInfos; // make available to console
console.log('Available input and output devices:', deviceInfos);
for (const deviceInfo of deviceInfos) {
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
if (deviceInfo.kind === 'videoinput') {
option.text = deviceInfo.label || `Camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
}
}
}
function getStream() {
if (window.stream) {
window.stream.getTracks().forEach(track => {
track.stop();
});
}
const videoSource = videoSelect.value;
const constraints = {
video: {deviceId: videoSource ? {exact: videoSource} : undefined}
};
return navigator.mediaDevices.getUserMedia(constraints).
then(gotStream).catch(handleError);
}
function gotStream(stream) {
window.stream = stream; // make stream available to console
videoSelect.selectedIndex = [...videoSelect.options].
findIndex(option => option.text === stream.getVideoTracks()[0].label);
videoElement.srcObject = stream;
}
function handleError(error) {
console.error('Error: ', error);
}
Then I want to start the scanner to the camara the user has selected because I want to obtaint the QR-CODE:
<script type="text/javascript">
let scanner = new Instascan.Scanner({ video: document.getElementById('preview'),mirror: false });
scanner.addListener('scan', function (content) {
alert("qr result " + content);
});
Instascan.Camera.getCameras().then(function (cameras) {
if (cameras.length > 0) {
scanner.start(cameras[0]); //I want to scanner.start the camara that the user has selected.
} else {
console.error('No cameras found.');
}
}).catch(function (e) {
console.error(e);
});
</script>

Local store video webRTC

I used the the information from the link (here) and got the following code, which helps me to record a video with my webcam. The code allows me to record a video and makes it available to download. However, I want to save the recorded video automatically to a local folder. How can I do that?
<script src="https://cdn.webrtc-experiment.com/RecordRTC.js"></script>
<section class="experiment">
<div class="inner">
<button style="float: right;" id="fit-to-screen">Fit to Screen!</button>
<label for="canvas-width-input">Canvas Width</label>
<input type="text" id="canvas-width-input" value="320">
<br />
<label for="canvas-height-input">Canvas Height</label>
<input type="text" id="canvas-height-input" value="240">
<br />
<div>
<button id="record-video">Record</button>
<button id="pause-resume-video" disabled>Pause</button>
<button id="stop-recording-video" disabled>Stop</button>
<hr>
<h2 id="video-url-preview"></h2>
<br>
<input type="checkbox" id="record-screen" style="width:auto;">
<label for="record-screen">Record Screen</label>
<br>
<video id="video" autoplay loop controls muted></video>
</div>
</div>
</section>
<script>
(function() {
var params = {},
r = /([^&=]+)=?([^&]*)/g;
function d(s) {
return decodeURIComponent(s.replace(/\+/g, ' '));
}
var match, search = window.location.search;
while (match = r.exec(search.substring(1)))
params[d(match[1])] = d(match[2]);
window.params = params;
})();
</script>
<script>
function getByID(id) {
return document.getElementById(id);
}
var recordVideo = getByID('record-video'),
pauseResumeVideo = getByID('pause-resume-video'),
stopRecordingVideo = getByID('stop-recording-video');
var canvasWidth_input = getByID('canvas-width-input'),
canvasHeight_input = getByID('canvas-height-input');
if(params.canvas_width) {
canvasWidth_input.value = params.canvas_width;
}
if(params.canvas_height) {
canvasHeight_input.value = params.canvas_height;
}
var video = getByID('video');
var videoConstraints = {
audio: false,
video: {
mandatory: {},
optional: []
}
};
</script>
<script>
var screen_constraints;
function isCaptureScreen(callback) {
if (document.getElementById('record-screen').checked) {
document.getElementById('fit-to-screen').onclick();
getScreenId(function (error, sourceId, _screen_constraints) {
if(error === 'not-installed') {
window.open('https://chrome.google.com/webstore/detail/screen-capturing/ajhifddimkapgcifgcodmmfdlknahffk');
}
if(error === 'permission-denied') {
alert('Screen capturing permission is denied.');
}
if(error === 'installed-disabled') {
alert('Please enable chrome screen capturing extension.');
}
if(_screen_constraints) {
screen_constraints = _screen_constraints.video;
videoConstraints = _screen_constraints;
}
else {
videoConstraints = screen_constraints;
}
callback();
});
}
else {
callback();
}
}
recordVideo.onclick = function() {
isCaptureScreen(function() {
recordVideoOrGIF(true);
});
};
function recordVideoOrGIF(isRecordVideo) {
navigator.getUserMedia(videoConstraints, function(stream) {
video.onloadedmetadata = function() {
video.width = canvasWidth_input.value || 320;
video.height = canvasHeight_input.value || 240;
var options = {
type: isRecordVideo ? 'video' : 'gif',
video: video,
canvas: {
width: canvasWidth_input.value,
height: canvasHeight_input.value
},
disableLogs: params.disableLogs || false,
recorderType: null // to let RecordRTC choose relevant types itself
};
recorder = window.RecordRTC(stream, options);
recorder.startRecording();
video.onloadedmetadata = false;
};
video.src = URL.createObjectURL(stream);
}, function() {
if (document.getElementById('record-screen').checked) {
if (location.protocol === 'http:')
alert('<https> is mandatory to capture screen.');
else
alert('Multi-capturing of screen is not allowed. Capturing process is denied. Are you enabled flag: "Enable screen capture support in getUserMedia"?');
} else
alert('Webcam access is denied.');
});
window.isAudio = false;
if (isRecordVideo) {
recordVideo.disabled = true;
stopRecordingVideo.disabled = false;
pauseResumeVideo.disabled = false;
}
}
stopRecordingVideo.onclick = function() {
this.disabled = true;
recordVideo.disabled = false;
if (recorder)
recorder.stopRecording(function(url) {
video.src = url;
video.play();
document.getElementById('video-url-preview').innerHTML = 'Recorded Video URL';
});
};
</script>
<script>
document.getElementById('fit-to-screen').onclick = function() {
this.disabled = true;
video.width = canvasWidth_input.value = innerWidth;
video.height = canvasHeight_input.value = innerHeight;
};
</script>
You will have to use the recorder.getBlob() method to get the actual blob of the video file, then send it to your server which will then save it to a file :
javascript:
stopRecordingVideo.onclick = function() {
this.disabled = true;
recordVideo.disabled = false;
if (recorder)
recorder.stopRecording(function(url) {
video.src = url;
video.play();
var recordedBlob = recorder.getBlob();
var formData = new FormData();
formData.append("videofile", recordedBlob);
var xhr = new XMLHttpRequest();
xhr.open("POST", "savevideofile.php");
xhr.send(formData);
document.getElementById('video-url-preview').innerHTML = '<a href="' + url + '>Recorded Video URL</a>';
});
};
savevideofile.php:
<?php
if($_FILES['videofile']){
$my_file = $_FILES['videofile'];
$my_blob = file_get_contents($my_file['tmp_name']);
file_put_contents('/path/to/your/file.webm', $my_blob);
}
?>
After looking at the source code of the web page you linked, the 'url' from your callback for stopRecording should be a downloadable URL.
All you'd have to do would be to take that URL and use it in an anchor with HTML5 download attribute, like so:
recorder.stopRecording(function(url) {
video.src = url;
video.play();
document.getElementById('video-url-preview').innerHTML = 'Recorded Video URL';
});

Polymer - reload core-list data

I wanted reload a core-list element to show new data, but it´s not refreshing.
I re-call the JS function thats generate the data but doesn t work... and reload like a 'normal' div doesn t work either! The list only shows the new data if i reload the entire page...
function values(sender, textomsg, datacriacao, senderfoto){
var sender2 = sender.split(",");
var textomsg2 = textomsg.split(",");
var datacriacao2 = datacriacao.split(",");
var senderfoto2 = senderfoto.split(",");
var namegen = {
generateString: function (inLength) {
var s = '';
for (var i = 0; i < inLength; i++) {
s += String.fromCharCode(Math.floor(Math.random() * 26) + 97);
}
return s;
},
generateName: function (inMin, inMax) {
return this.generateString(Math.floor(Math.random() * (inMax - inMin + 1) + inMin));
}
};
Polymer('list-test', {
count: sender.length,
ready: function () {
this.data = this.generateData();
},
generateData: function () {
var names = [], data = [];
for (var i = 0; i < this.count; i++) {
names.push(namegen.generateName(4, 8));
}
names.sort();
for (var i = 0; i < this.count; i++) {
data.push({
index: i,
sender: sender2[i],
textomsg: textomsg2[i],
datacriacao: datacriacao2[i],
senderfoto: senderfoto2[i]
});
}
return data;
},
tapAction: function (e) {
console.log('tap', e);
}
});
}
<%----%>
<template id="templateConversas" runat="server">
<div id="item" class="item {{ {selected: selected} | tokenList }}" ><%--onClick="conversa('{{name}}');"--%>
<div class="message" style="background-image: url({{senderfoto}});">
<span class="from"><br/>{{sender}}</span>
<span class="timestamp">{{datacriacao}}</span>
<div class="subject"><br/>{{textomsg}} </div><%--------Infinite List. {{index}}--%>
<%--<div class="body"><br/>Mensagem de teste...........</div>--%>
</div>
</div>
</template>
The problem is also reload the 'list-test'. if i call the js function after the list is loaded it doesn't apply the new data...
Your code isn't complete so it is hard to understand but I think that the problem is that you don't assign the result of the generateData() function to the template's model. Try following script for your component
Polymer('list-test', {
created: function () {
this.data = [];
},
refresh: function () {
this.data = this.generateData();
},
generateData: function () {
// your original code here
}
});
Now the list content should be updated with newly generated data when you call refresh() of the list-test element. To fill the list when element is created add
ready: function () {
this.refresh();
},

Codeception acceptance test error for save/reset

I am trying to perform acceptance tests for my website using Codeception, and I am experiencing a strange error due to a reset button on the form I am testing. Basically, my test for clicking on 'Save' works only if either the reset button on my form is AFTER the Save button, or if the reset button is left off the form altogether. If the reset button is inserted in the form before the save button, Codeception throws an Unreachable field "reset" error. Here is my Codeception code:
<?php
$I = new WebGuy($scenario);
$I->wantTo('find an employee in the database');
$I->amOnPage('/employees/find/');
$I->fillField('employeeLookup[first_name]', 'Sergi');
$I->click('Save', '#employeeLookup_save');
$I->see('Based on your search for Sergi, the following employees were found:');
$I->see('Remmele');
$I->see('Feb 28 1992');
And here is my HTML (much of it being generated from Symfony2):
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Find existing employee</title>
</head>
<body>
<div id="content">
<p>Hello, enter either the first name, or the last name of the employee
you are searching for.</p>
<form name="employeeLookup" method="post" action="">
<div><label for="employeeLookup_first_name" class="required">Name: </label><input type="text" id="employeeLookup_first_name" name="employeeLookup[first_name]" required="required" /></div>
<div><button type="reset" id="employeeLookup_reset" name="employeeLookup[reset]">Reset</button></div>
<div><button type="submit" id="employeeLookup_save" name="employeeLookup[save]">Save</button></div>
<input type="hidden" id="employeeLookup__token" name="employeeLookup[_token]" value="RcpMVTGgB6WhKgDoXXRwmV_l4AFYKWTZko-dnBDhhvM" /></form>
</div>
<div id="sfwdte5d291" class="sf-toolbar" style="display: none"></div><script>/*<![CDATA[*/ Sfjs = (function() { "use strict"; var noop = function() {}, profilerStorageKey = 'sf2/profiler/', request = function(url, onSuccess, onError, payload, options) { var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); options = options || {}; xhr.open(options.method || 'GET', url, true); xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); xhr.onreadystatechange = function(state) { if (4 === xhr.readyState && 200 === xhr.status) { (onSuccess || noop)(xhr); } else if (4 === xhr.readyState && xhr.status != 200) { (onError || noop)(xhr); } }; xhr.send(payload || ''); }, hasClass = function(el, klass) { return el.className.match(new RegExp('\\b' + klass + '\\b')); }, removeClass = function(el, klass) { el.className = el.className.replace(new RegExp('\\b' + klass + '\\b'), ' '); }, addClass = function(el, klass) { if (!hasClass(el, klass)) { el.className += " " + klass; } }, getPreference = function(name) { if (!window.localStorage) { return null; } return localStorage.getItem(profilerStorageKey + name); }, setPreference = function(name, value) { if (!window.localStorage) { return null; } localStorage.setItem(profilerStorageKey + name, value); }; return { hasClass: hasClass, removeClass: removeClass, addClass: addClass, getPreference: getPreference, setPreference: setPreference, request: request, load: function(selector, url, onSuccess, onError, options) { var el = document.getElementById(selector); if (el && el.getAttribute('data-sfurl') !== url) { request( url, function(xhr) { el.innerHTML = xhr.responseText; el.setAttribute('data-sfurl', url); removeClass(el, 'loading'); (onSuccess || noop)(xhr, el); }, function(xhr) { (onError || noop)(xhr, el); }, options ); } return this; }, toggle: function(selector, elOn, elOff) { var i, style, tmp = elOn.style.display, el = document.getElementById(selector); elOn.style.display = elOff.style.display; elOff.style.display = tmp; if (el) { el.style.display = 'none' === tmp ? 'none' : 'block'; } return this; } } })();/*]]>*/</script><script>/*<![CDATA[*/ (function () { Sfjs.load( 'sfwdte5d291', '/employees/app_dev.php/_wdt/e5d291', function(xhr, el) { el.style.display = -1 !== xhr.responseText.indexOf('sf-toolbarreset') ? 'block' : 'none'; if (el.style.display == 'none') { return; } if (Sfjs.getPreference('toolbar/displayState') == 'none') { document.getElementById('sfToolbarMainContent-e5d291').style.display = 'none'; document.getElementById('sfToolbarClearer-e5d291').style.display = 'none'; document.getElementById('sfMiniToolbar-e5d291').style.display = 'block'; } else { document.getElementById('sfToolbarMainContent-e5d291').style.display = 'block'; document.getElementById('sfToolbarClearer-e5d291').style.display = 'block'; document.getElementById('sfMiniToolbar-e5d291').style.display = 'none'; } }, function(xhr) { if (xhr.status !== 0) { confirm('An error occurred while loading the web debug toolbar (' + xhr.status + ': ' + xhr.statusText + ').\n\nDo you want to open the profiler?') && (window.location = '/employees/app_dev.php/_profiler/e5d291'); } } ); })();/*]]>*/</script>
</body>
</html>
Finally, here is the relevant output of the error message from Codeception:
1) Failed to find an employee in the database in FindEmployeeCept.php
Sorry, I couldn't click "Save","#employeeLookup_save":
Behat\Mink\Exception\ElementException: Exception thrown by ((//html/descendant-or-self::*[#id = 'employeeLookup_save'])[1]/.//input[./#type = 'submit' or ./#type = 'image' or ./#type = 'button'][(((./#id = 'Save' or ./#name = 'Save') or contains(./#value, 'Save')) or contains(./#title, 'Save'))] | .//input[./#type = 'image'][contains(./#alt, 'Save')] | .//button[((((./#id = 'Save' or ./#name = 'Save') or contains(./#value, 'Save')) or contains(normalize-space(string(.)), 'Save')) or contains(./#title, 'Save'))] | .//input[./#type = 'image'][contains(./#alt, 'Save')] | .//*[./#role = 'button'][(((./#id = 'Save' or ./#name = 'Save') or contains(./#value, 'Save')) or contains(./#title, 'Save') or contains(normalize-space(string(.)), 'Save'))])[1]
Unreachable field "reset"
Again, if the reset button is rendered after the save button in the HTML, the acceptance tests pass just fine. Also, if the reset button is left off of the form entirely, the acceptance test passes as well. Does anyone have any idea what is causing this?

Polyfill HTML5 form attribute (for input fields)

This is the markup I use:
<input type="text" form="myform" name="inp1" />
<form id="myform" name="myform">
...
</form>
Now I realized that it does not work for old IE and therefore I am searching for a HTML 5 polyfill.
Anyone aware of a certain polyfill which covers this HTML5 feature?
I wrote this polyfill to emulate such feature by duplicating fields upon form submission, tested in IE6 and it worked fine.
(function($) {
/**
* polyfill for html5 form attr
*/
// detect if browser supports this
var sampleElement = $('[form]').get(0);
var isIE11 = !(window.ActiveXObject) && "ActiveXObject" in window;
if (sampleElement && window.HTMLFormElement && (sampleElement.form instanceof HTMLFormElement || sampleElement instanceof window.HTMLFormElement) && !isIE11) {
// browser supports it, no need to fix
return;
}
/**
* Append a field to a form
*
*/
$.fn.appendField = function(data) {
// for form only
if (!this.is('form')) return;
// wrap data
if (!$.isArray(data) && data.name && data.value) {
data = [data];
}
var $form = this;
// attach new params
$.each(data, function(i, item) {
$('<input/>')
.attr('type', 'hidden')
.attr('name', item.name)
.val(item.value).appendTo($form);
});
return $form;
};
/**
* Find all input fields with form attribute point to jQuery object
*
*/
$('form[id]').submit(function(e) {
// serialize data
var data = $('[form='+ this.id + ']').serializeArray();
// append data to form
$(this).appendField(data);
}).each(function() {
var form = this,
$fields = $('[form=' + this.id + ']');
$fields.filter('button, input').filter('[type=reset],[type=submit]').click(function() {
var type = this.type.toLowerCase();
if (type === 'reset') {
// reset form
form.reset();
// for elements outside form
$fields.each(function() {
this.value = this.defaultValue;
this.checked = this.defaultChecked;
}).filter('select').each(function() {
$(this).find('option').each(function() {
this.selected = this.defaultSelected;
});
});
} else if (type.match(/^submit|image$/i)) {
$(form).appendField({name: this.name, value: this.value}).submit();
}
});
});
})(jQuery);
The polyfill above doesn't take into account the Edge browser. I have amended it to use feature detection, which I have tested in IE7+, Edge, Firefox (mobile/desktop), Chrome (mobile/desktop), Safari (mobile/desktop), and Android browser 4.0.
(function($) {
/**
* polyfill for html5 form attr
*/
// detect if browser supports this
var SAMPLE_FORM_NAME = "html-5-polyfill-test";
var sampleForm = $("<form id='" + SAMPLE_FORM_NAME + "'/>");
var sampleFormAndHiddenInput = sampleForm.add($("<input type='hidden' form='" + SAMPLE_FORM_NAME + "'/>"));
sampleFormAndHiddenInput.prependTo('body');
var sampleElementFound = sampleForm[0].elements[0];
sampleFormAndHiddenInput.remove();
if (sampleElementFound) {
// browser supports it, no need to fix
return;
}
/**
* Append a field to a form
*
*/
$.fn.appendField = function(data) {
// for form only
if (!this.is('form')) return;
// wrap data
if (!$.isArray(data) && data.name && data.value) {
data = [data];
}
var $form = this;
// attach new params
$.each(data, function(i, item) {
$('<input/>')
.attr('type', 'hidden')
.attr('name', item.name)
.val(item.value).appendTo($form);
});
return $form;
};
/**
* Find all input fields with form attribute point to jQuery object
*
*/
$('form[id]').submit(function(e) {
// serialize data
var data = $('[form='+ this.id + ']').serializeArray();
// append data to form
$(this).appendField(data);
}).each(function() {
var form = this,
$fields = $('[form=' + this.id + ']');
$fields.filter('button, input').filter('[type=reset],[type=submit]').click(function() {
var type = this.type.toLowerCase();
if (type === 'reset') {
// reset form
form.reset();
// for elements outside form
$fields.each(function() {
this.value = this.defaultValue;
this.checked = this.defaultChecked;
}).filter('select').each(function() {
$(this).find('option').each(function() {
this.selected = this.defaultSelected;
});
});
} else if (type.match(/^submit|image$/i)) {
$(form).appendField({name: this.name, value: this.value}).submit();
}
});
});
})(jQuery);
I improved patstuart's polyfill, such that:
a form can now be submitted several times, e.g. when using the target attribute (external fields were duplicated previously)
reset buttons now work properly
Here it is:
(function($) {
/**
* polyfill for html5 form attr
*/
// detect if browser supports this
var SAMPLE_FORM_NAME = "html-5-polyfill-test";
var sampleForm = $("<form id='" + SAMPLE_FORM_NAME + "'/>");
var sampleFormAndHiddenInput = sampleForm.add($("<input type='hidden' form='" + SAMPLE_FORM_NAME + "'/>"));
sampleFormAndHiddenInput.prependTo('body');
var sampleElementFound = sampleForm[0].elements[0];
sampleFormAndHiddenInput.remove();
if (sampleElementFound) {
// browser supports it, no need to fix
return;
}
/**
* Append a field to a form
*
*/
var CLASS_NAME_POLYFILL_MARKER = "html-5-polyfill-form-attr-marker";
$.fn.appendField = function(data) {
// for form only
if (!this.is('form')) return;
// wrap data
if (!$.isArray(data) && data.name && data.value) {
data = [data];
}
var $form = this;
// attach new params
$.each(data, function(i, item) {
$('<input/>')
.attr('type', 'hidden')
.attr('name', item.name)
.attr('class', CLASS_NAME_POLYFILL_MARKER)
.val(item.value).appendTo($form);
});
return $form;
};
/**
* Find all input fields with form attribute point to jQuery object
*
*/
$('form[id]').submit(function(e, origSubmit) {
// clean up form from last submit
$('.'+CLASS_NAME_POLYFILL_MARKER, this).remove();
// serialize data
var data = $('[form='+ this.id + ']').serializeArray();
// add data from external submit, if needed:
if (origSubmit && origSubmit.name)
data.push({name: origSubmit.name, value: origSubmit.value})
// append data to form
$(this).appendField(data);
})
//submit and reset behaviour
$('button[type=reset], input[type=reset]').click(function() {
//extend reset buttons to fields with matching form attribute
// reset form
var formId = $(this).attr("form");
var formJq = $('#'+formId);
if (formJq.length)
formJq[0].reset();
// for elements outside form
if (!formId)
formId = $(this).closest("form").attr("id");
$fields = $('[form=' + formId + ']');
$fields.each(function() {
this.value = this.defaultValue;
this.checked = this.defaultChecked;
}).filter('select').each(function() {
$(this).find('option').each(function() {
this.selected = this.defaultSelected;
});
});
});
$('button[type=submit], input[type=submit], input[type=image]').click(function() {
var formId = $(this).attr("form") || $(this).closest("form").attr("id");
$('#'+formId).trigger('submit', this); //send clicked submit as extra parameter
});
})(jQuery);
after reading thru the docs of webshim it seems it has a polyfill for that.
http://afarkas.github.io/webshim/demos/demos/webforms.html
I made a vanilla JavaScript polyfill based on the above polyfills and uploaded it on GitHub: https://github.com/Ununnilium/form-attribute-polyfill.
I also added a custom event to handle the case when submit is processed by JavaScript and not directly by the browser. I tested the code only shortly with IE 11, so please check it yourself before use. The polling should maybe be replaced by a more efficient detection function.
function browserNeedsPolyfill() {
var TEST_FORM_NAME = "form-attribute-polyfill-test";
var testForm = document.createElement("form");
testForm.setAttribute("id", TEST_FORM_NAME);
testForm.setAttribute("type", "hidden");
var testInput = document.createElement("input");
testInput.setAttribute("type", "hidden");
testInput.setAttribute("form", TEST_FORM_NAME);
testForm.appendChild(testInput);
document.body.appendChild(testInput);
document.body.appendChild(testForm);
var sampleElementFound = testForm.elements.length === 1;
document.body.removeChild(testInput);
document.body.removeChild(testForm);
return !sampleElementFound;
}
// Ideas from jQuery form attribute polyfill https://stackoverflow.com/a/26696165/2372674
function executeFormPolyfill() {
function appendDataToForm(data, form) {
Object.keys(data).forEach(function(name) {
var inputElem = document.createElement("input");
inputElem.setAttribute("type", "hidden");
inputElem.setAttribute("name", name);
inputElem.value = data[name];
form.appendChild(inputElem);
});
}
var forms = document.body.querySelectorAll("form[id]");
Array.prototype.forEach.call(forms, function (form) {
var fields = document.querySelectorAll('[form="' + form.id + '"]');
var dataFields = [];
Array.prototype.forEach.call(fields, function (field) {
if (field.disabled === false && field.hasAttribute("name")) {
dataFields.push(field);
}
});
Array.prototype.forEach.call(fields, function (field) {
if (field.type === "reset") {
field.addEventListener("click", function () {
form.reset();
Array.prototype.forEach.call(dataFields, function (dataField) {
if (dataField.nodeName === "SELECT") {
Array.prototype.forEach.call(dataField.querySelectorAll('option'), function (option) {
option.selected = option.defaultSelected;
});
} else {
dataField.value = dataField.defaultValue;
dataField.checked = dataField.defaultChecked;
}
});
});
} else if (field.type === "submit" || field.type === "image") {
field.addEventListener("click", function () {
var obj = {};
obj[field.name] = field.value;
appendDataToForm(obj, form);
form.dispatchEvent(eventToDispatch);
});
}
});
form.addEventListener("submit", function () {
var data = {};
Array.prototype.forEach.call(dataFields, function (dataField) {
data[dataField.name] = dataField.value;
});
appendDataToForm(data, form);
});
});
}
// Poll for new forms and execute polyfill for them
function detectedNewForms() {
var ALREADY_DETECTED_CLASS = 'form-already-detected';
var newForms = document.querySelectorAll('form:not([class="' + ALREADY_DETECTED_CLASS + '"])');
if (newForms.length !== 0) {
Array.prototype.forEach.call(newForms, function (form) {
form.className += ALREADY_DETECTED_CLASS;
});
executeFormPolyfill();
}
setTimeout(detectedNewForms, 100);
}
// Source: https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent
function polyfillCustomEvent() {
if (typeof window.CustomEvent === "function") {
return false;
}
function CustomEvent(event, params) {
params = params || {bubbles: false, cancelable: false, detail: undefined};
var evt = document.createEvent('CustomEvent');
evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
}
if (browserNeedsPolyfill()) {
polyfillCustomEvent(); // IE is missing CustomEvent
// This workaround is needed if submit is handled by JavaScript instead the browser itself
// Source: https://stackoverflow.com/a/35155789/2372674
var eventToDispatch = new CustomEvent("submit", {"bubbles": true, "cancelable": true});
detectedNewForms(); // Poll for new forms and execute form attribute polyfill for new forms
}
I take some time to send an update for this polyfill because it doesn't work with MS Edge.
I add 2 line to fix it :
var isEdge = navigator.userAgent.indexOf("Edge");
if (sampleElement && window.HTMLFormElement && sampleElement.form instanceof HTMLFormElement && !isIE11 && isEdge == -1) {
// browser supports it, no need to fix
return;
}
UPDATE: Edge now support it:
https://caniuse.com/#feat=form-attribute