Alert, Confirm, and Prompt Not Working After Using History API on Safari, iOS

JavaScript dialogs alert(), confirm() and prompt() in cross origin iframe does not work any longer

This is absurd and subjective decision of Google to remove alert(), confirm(), and prompt() for cross origin iframes. And they called it "feature". And justification is very poor - see "motivation" bellow. A very weak reason for removing such an important feature! Community and developers should protest!

Problem

https://www.chromestatus.com/feature/5148698084376576

Feature: Remove alert(), confirm(), and prompt for cross origin iframes

Chrome allows iframes to trigger Javascript dialogs, it shows “ says ...” when the iframe is the same origin as the top frame, and “An embedded page on this page says...” when the iframe is cross-origin. The current UX is confusing, and has previously led to spoofs where sites pretend the message comes from Chrome or a different website. Removing support for cross origin iframes’ ability to trigger the UI will prevent this kind of spoofing, and unblock further UI simplifications.

Motivation

The current UI for JS dialogs (in general, not just for the cross-origin subframe case) is confusing, because the message looks like the browser’s own UI. This has led to spoofs (particularly with window.prompt) where sites pretend that a particular message is coming from Chrome (e.g. 1,2,3). Chrome mitigates these spoofs by prefacing the message with “ says...”. However, when these alerts are coming from a cross-origin iframe, the UI is even more confusing because Chrome tries to explain the dialog is not coming from the browser itself or the top level page. Given the low usage of cross-origin iframe JS dialogs, the fact that when JS dialogs are used they are generally not required for the site’s primary functionality, and the difficulty in explaining reliably where the dialog is coming from, we propose removing JS dialogs for cross-origin iframes. This will also unblock our ability to further simplify the dialog by removing the hostname indication and making the dialog more obviously a part of the page (and not the browser) by moving it to the center of the content area. These changes are blocked on removing cross-origin support for JS dialogs, since otherwise these subframes could pretend their dialog is coming from the parent page.

Solution

Send message via Window.postMessage() from iframe to parent and show dialog via parent page. It is very elegant hack and shame on Google because before Chrome version 92 client saw alert dialog e.g. An embedded page iframe.com" says: ... (which was correct - client see real domain which invoked alert) but now with postMessage solution client will see a lie e.g. The page example.com" says: ... but alert was not invoked by example.com. Stupid google decision caused them to achieve the opposite effect - client will be much more confused now. Google's decision was hasty and they didn't think about the consequences. In case of prompt() and confirm() it is a little bit tricky via Window.postMessage() because we need to send result from top back to iframe.

What Google will do next? Disable Window.postMessage()? Déjà vu. We are back in Internet Explorer era... developers waste time by doing stupid hacks.

TL;DR: Demo

https://domain-a.netlify.app/parent.html

Code

With code bellow you can use overridden native alert(), confirm() and prompt() in cross origin iframe with minimum code change. There is no change for alert() usage. I case of confirm() and prompt() just add "await" keyword before it or feel free to use callback way in case you can not switch easily your sync functions to async functions. See all usage examples in iframe.html bellow.

Everything bad comes with something good - now I gained with this solution an advantage that iframe domain is not revealed (domain from address bar is now used in dialogs).

https://example-a.com/parent.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Parent (domain A)</title>
<script type="text/javascript" src="dialogs.js"></script>
</head>
<body>
<h1>Parent (domain A)</h1>
<iframe src="https://example-b.com/iframe.html">
</body>
</html>

https://example-b.com/iframe.html

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Iframe (domain B)</title>
<script type="text/javascript" src="dialogs.js"></script>
</head>
<body>
<h1>Iframe (domain B)</h1>
<script type="text/javascript">
alert('alert() forwarded from iframe.html');

confirm('confirm() forwarded from iframe.html via callback', (result) => {
console.log('confirm() result via callback: ', result);
});

prompt('prompt() forwarded from iframe.html via callback', null, (result) => {
console.log('prompt() result via callback: ', result);
});

(async () => {
var result1 = await confirm('confirm() forwarded from iframe.html via promise');
console.log('confirm() result via promise: ', result1);

var result2 = await prompt('prompt() forwarded from iframe.html via promise');
console.log('prompt() result via promise: ', result2);
})();
</script>
</body>
</html>

dialogs.js

(function() {

var id = 1,
store = {},
isIframe = (window === window.parent || window.opener) ? false : true;

// Send message
var sendMessage = function(windowToSend, data) {
windowToSend.postMessage(JSON.stringify(data), '*');
};

// Helper for overridden confirm() and prompt()
var processInteractiveDialog = function(data, callback) {
sendMessage(parent, data);

if (callback)
store[data.id] = callback;
else
return new Promise(resolve => { store[data.id] = resolve; })
};

// Override native dialog functions
if (isIframe) {
// alert()
window.alert = function(message) {
var data = { event : 'dialog', type : 'alert', message : message };
sendMessage(parent, data);
};

// confirm()
window.confirm = function(message, callback) {
var data = { event : 'dialog', type : 'confirm', id : id++, message : message };
return processInteractiveDialog(data, callback);
};

// prompt()
window.prompt = function(message, value, callback) {
var data = { event : 'dialog', type : 'prompt', id : id++, message : message, value : value || '' };
return processInteractiveDialog(data, callback);
};
}

// Listen to messages
window.addEventListener('message', function(event) {
try {
var data = JSON.parse(event.data);
}
catch (error) {
return;
}

if (!data || typeof data != 'object')
return;

if (data.event != 'dialog' || !data.type)
return;

// Initial message from iframe to parent
if (!isIframe) {
// alert()
if (data.type == 'alert')
alert(data.message)

// confirm()
else if (data.type == 'confirm') {
var data = { event : 'dialog', type : 'confirm', id : data.id, result : confirm(data.message) };
sendMessage(event.source, data);
}

// prompt()
else if (data.type == 'prompt') {
var data = { event : 'dialog', type : 'prompt', id : data.id, result : prompt(data.message, data.value) };
sendMessage(event.source, data);
}
}

// Response message from parent to iframe
else {
// confirm()
if (data.type == 'confirm') {
store[data.id](data.result);
delete store[data.id];
}

// prompt()
else if (data.type == 'prompt') {
store[data.id](data.result);
delete store[data.id];
}
}
}, false);

})();

Warn user before leaving web page with unsaved changes

Short, wrong answer:

You can do this by handling the beforeunload event and returning a non-null string:

window.addEventListener("beforeunload", function (e) {
var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';

(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});

The problem with this approach is that submitting a form is also firing the unload event. This is fixed easily by adding the a flag that you're submitting a form:

var formSubmitting = false;
var setFormSubmitting = function() { formSubmitting = true; };

window.onload = function() {
window.addEventListener("beforeunload", function (e) {
if (formSubmitting) {
return undefined;
}

var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';

(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
};

Then calling the setter when submitting:

<form method="post" onsubmit="setFormSubmitting()">     
<input type="submit" />
</form>

But read on...

Long, correct answer:

You also don't want to show this message when the user hasn't changed anything on your forms. One solution is to use the beforeunload event in combination with a "dirty" flag, which only triggers the prompt if it's really relevant.

var isDirty = function() { return false; }

window.onload = function() {
window.addEventListener("beforeunload", function (e) {
if (formSubmitting || !isDirty()) {
return undefined;
}

var confirmationMessage = 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.';

(e || window.event).returnValue = confirmationMessage; //Gecko + IE
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
});
};

Now to implement the isDirty method, there are various approaches.

You can use jQuery and form serialization, but this approach has some flaws. First you have to alter the code to work on any form ($("form").each() will do), but the greatest problem is that jQuery's serialize() will only work on named, non-disabled elements, so changing any disabled or unnamed element will not trigger the dirty flag. There are workarounds for that, like making controls readonly instead of enabling, serializing and then disabling the controls again.

So events seem the way to go. You can try listening for keypresses. This event has a few issues:

  • Won't trigger on checkboxes, radio buttons, or other elements that are being altered through mouse input.
  • Will trigger for irrelevant keypresses like the Ctrl key.
  • Won't trigger on values set through JavaScript code.
  • Won't trigger on cutting or pasting text through context menus.
  • Won't work for virtual inputs like datepickers or checkbox/radiobutton beautifiers which save their value in a hidden input through JavaScript.

The change event also doesn't trigger on values set from JavaScript code, so also won't work for virtual inputs.

Binding the input event to all inputs (and textareas and selects) on your page won't work on older browsers and, like all event handling solutions mentioned above, doesn't support undo. When a user changes a textbox and then undoes that, or checks and unchecks a checkbox, the form is still considered dirty.

And when you want to implement more behavior, like ignoring certain elements, you'll have even more work to do.

Don't reinvent the wheel:

So before you think about implementing those solutions and all required workarounds, realize you're reinventing the wheel and you're prone to running into problems others have already solved for you.

If your application already uses jQuery, you may as well use tested, maintained code instead of rolling your own, and use a third-party library for all of this.

jquery.dirty (suggested by @troseman in the comments) provides functions for properly detecting whether a form has been changed or not, and preventing the user from leaving the page while displaying a prompt. It also has other useful functions like resetting the form, and setting the current state of the form as the "clean" state. Example usage:

$("#myForm").dirty({preventLeaving: true});

An older, currently abandoned project, is jQuery's Are You Sure? plugin, which also works great; see their demo page. Example usage:

<script src="jquery.are-you-sure.js"></script>

<script>
$(function() {
$('#myForm').areYouSure(
{
message: 'It looks like you have been editing something. '
+ 'If you leave before saving, your changes will be lost.'
}
);
});

</script>

Custom messages not supported everywhere

Do note that since 2011 already, Firefox 4 didn't support custom messages in this dialog. As of april 2016, Chrome 51 is being rolled out in which custom messages are also being removed.

Some alternatives exist elsewhere on this site, but I think a dialog like this is clear enough:

Do you want to leave this site?

Changes you made may not be saved.

Leave Stay

Recording Video HTML5 not working in Safari and iOS mobile app

I don't think recording webcam as video is an available option on Apple devices.

It might be a licensing issue rather than a technical limitation.

It works perfectly in Chrome browser but does not seem to be working
on Safari and iOS mobile app. On debugging both captureStream() and
mozCaptureStream() functions are undefined.

  1. Your link is recording into Google's own webM video which is a format not supported by Apple. The Safari browser cannot encode pixels as VP8 or VP9 to use inside webM container. Apple has an MPEG video license so for Safari, I expect MP4 to be their expected output (but is there a free H264 encoder inside Safari? Nope).

  2. mozCaptureStream() is specific to Mozilla Firefox. Safari won't know/accept it.

  3. captureStream() is not fully suppported on Safari. The missing part is the recording part.

Possible workarounds:

  • Try enabling MediaRecorder API in Safari settings.

    Research (double-check) for known issues, like as mentioned at bottom of this read this article: https://blog.addpipe.com/safari-support-on-macos-beta/

  • If you can draw webcam to Canvas then consider bringing in your own (Javascript-based) H264 encoder (for MP4 video) or VP8/VP9 encoder (for webM video).

  • You can try: https://github.com/TrevorSundberg/h264-mp4-encoder.

  • Wait for webCodecs API to be added to Safari.
    Note in Chrome/Edge you can just use the built-in webCodecs API to encode video for MP4 or WebM formats. Safari has no free encoders and thus has no webCodecs API.



Related Topics



Leave a reply



Submit