I need to know what should be done to use JavaScript in a HTML sitting in UIWebView to notify Objective-C that something has happened?
To be more exact, I'm playing some JavaScript animation in HTML and I need to alert the Objective-C code that the animation has ended.
There seems to be no official method of doing this. However, the standard workaround involves reading and parsing incoming URL requests, basically rolling your own serialized messaging protocol. The message handling should be done in the webView:shouldStartLoadWithRequest:navigationType method of your view controller.
Note: there are several free libraries (PhoneGap, QuickConnect, JS-to-Cocoa Bridge) which wrap this functionality (plus do a whole lot more). To reinvent the wheel (or know why it's round, so to speak), read on.
From JavaScript, you will invoke the callback by attempting to navigate to a new URL:
// In JavaScript
window.location = 'myapp:myaction:param1:param2'; // etc...
In Objective-C, implement the UIWebViewDelegate protocol in your .h file:
// In your header file
#interface MyAppViewController : UIViewController <UIWebViewDelegate> {
...
}
#end
Next, implement the method in your .m file:
// In your implementation file
-(BOOL)webView:(UIWebView *)webView2
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
// Break apart request URL
NSString *requestString = [[request URL] absoluteString];
NSArray *components = [requestString componentsSeparatedByString:#":"];
// Check for your protocol
if ([components count] > 1 &&
[(NSString *)[components objectAtIndex:0] isEqualToString:#"myapp"])
{
// Look for specific actions
if ([(NSString *)[components objectAtIndex:1] isEqualToString:#"myaction"])
{
// Your parameters can be found at
// [components objectAtIndex:n]
// where 'n' is the ordinal position of the colon-delimited parameter
}
// Return 'NO' to prevent navigation
return NO;
}
// Return 'YES', navigate to requested URL as normal
return YES;
}
Two important notes:
Context: navigating to myapp:whatever will (of course) fail under any other context. Keep this in mind if you're loading cross-platform pages.
Timing: if a second window.location = call is made before the first returns, it will get 'lost.' So, either lump your calls together, manually delay execution, or implement a queue which combines the above with JS queries into Objective-C objects.
Actually for timing in iOS (maybe not for OSX?), if a second window.location call is made before the previous window.location call executes, then the first window.location call gets lost. I think the window.location call executes asynchronisely with the JavaScript after it is called, and if another call is made it before it executes, it cancels the first.
For example, when capturing touch events, I have seen ontouchstart not get sent via window.location, if an ontouchmove event occurs to quickly afterwards (such as in a fast finger swipe). Thus your Objective-C doesn't get the ontouchstart event. This is more of a problem on the original iPad than the iPad2, I assume because of processing speed.
zourtney's answer is correct , but forgot to mention one thing .. needed to register delegate to webview by
- (void)viewDidLoad {
[super viewDidLoad]; --- instantiate _webview next .. then
_webview.delegate = self; //important .. needed to register webview to delegate
}
hope this helps .....
Swift Version
class ViewController: UIViewController,UIWebViewDelegate{
#IBOutlet weak var webviewInstance: UIWebView!
override func viewDidLoad() {
webviewInstance.delegate = self
super.viewDidLoad()
}
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
let requestString: String = (request.URL?.absoluteString)!
var components: [AnyObject] = requestString.componentsSeparatedByString(":")
// Check for your protocol
if components.count > 1 && (String(components[0]) == "myapp") {
// Look for specific actions
if (String(components[1]) == "myaction") {
// Your parameters can be found at
// [components objectAtIndex:n]
}
return false
}
return true
}
}
Related
I can't seem to get this to work.
In response to a click, window A opens window B (which then has focus). Then, in response to a click on B, the window calls window.opener.focus(), but the focus does not go back to A.
I have found a strange, strange workaround for Chrome (29, possibly others).
If I run:
window.opener.name = 'somename';
window.open(window.opener.location.href, window.opener.name);
window.opener.focus();
it does work (and doesn't reload window A). But this doesn't work for Firefox, and it is probably a fluke anyway.
It seems very clear to me what opener and focus are supposed to do, but window.opener.focus() doesn't work. What am I missing?
From the fine manual:
Makes a request to bring the window to the front. It may fail due to user settings and the window isn't guaranteed to be frontmost before this method returns.
Emphasis mine. Calling focus() is just a request and the browser is free to ignore you and you should generally expect to be ignored. Consider what sorts of nefarious things you could get up to by switching focus to a tiny window while someone is typing if you need some reasons why a browser would ignore your request.
If you need focus() to work for your application to work then you need to redesign your application so that it doesn't need to call focus().
I can see why a browser/OS will not allow a child windows to take over the focus (abuse of power). Here is a workaround:
In the parent window, declare a function in "window.external" that will trigger Javascript "alert()" or "confirm()".
Invoke that function from the child window.
The browser might ignore a request from a child window that wants to control the focus (e.g. window.opener.focus()), but the browser should honor a request from a parent window that triggers an alert() or a confirm() action, which requires to focus on the parent window.
JS Parent:
var child = window.open('child.html', 'child');
window.external.comeback = function() {
var back = confirm('Are you sure you want to comback?');
if(back) {
child.close();
} else {
child.focus();
}
}
JS Child:
// assuming you have jQuery
$('.btn').click() {
window.opener.external.comeback();
};
--I am using this code in a real world application to handle a checkout request that runs in child window, and I need to gracefully return to the parent window.
Web-facing or private intranet?
Window management is up to the Browser and the OS. HTML & ECMAscript have nothing to say about it.
If this is for a public-facing website, then just don't bother -- as they say, "Don't break the web."
But I really wanna!
If this is for some tightly managed (say Intranet) application of some kind then you'll need to resort to writing Addons/Extensions. It's certaintly easier if you can restrict yourself to a single browser & platform.
EDIT: Example for Firefox on Win32...
This solution works as a custom addon for Firefox which uses jsctypes internally to load a Win32 DLL. The window_focus() JavaScript function is exposed which does what you want.
There are 3 parts to this solution:
The privileged JavaScript code to load/bind the Win32 APIs
The CPP header file for our external DLL
The CPP source file for our external DLL
I built a simple GUI DLL project in MSVC++ with the later two files & compiled wmctrl.dll, depending on msvcr100.dll, and used Dependency Walker to find the "plain C" symbols exported by the DLL for use by js-ctypes. E.g: ?wmctrl_find_window##YAKPAD#Z is the "plain C" symbol for the C++ api function called wmctrl_find_window.
As a caveat, this code relies on temporarily being able to change the title of the window that needs to be focused so that Win32 APIs can examine all windows on your desktop to find the correct Firefox window.
You need to have access to privileged Mozilla platform APIs, i.e: JavaScript inside a Firefox Addon.
In your privileged JavaScript code:
// get API constants (might already be available)
const {Cc,Ci,Cu} = require("chrome");
// import js-ctypes
var file=null, lib=null, ctypes = {};
Cu.import("resource://gre/modules/ctypes.jsm", ctypes);
var ctypes = ctypes.ctypes;
// build platform specific library path
var filename = ctypes.libraryName("wmctrl"); // automatically adds '.dll'
var comp = "#mozilla.org/file/directory_service;1";
var file = Cc[comp].getService(Ci.nsIProperties).get("CurProcD", Ci.nsIFile);
file.append("browser_code"); // or whereever you put your DLL
file.append(filename);
// get the JavaScript library interface (load the library)
var lib = ctypes.open(file.path);
// wmctrl_find_window: returing unsigned 32bit (long) "window handle"
// takes string "window title".
var find_window = lib.declare(
"?wmctrl_find_window##YAKPAD#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.uint32_t, /* return type: uint32 */
ctypes.char.ptr); /* parameter: string */
// wmctrl_window_focus: takes unsigned 32bit (long) "window handle".
var window_focus = lib.declare(
"?wmctrl_window_focus##YAXK#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.void_t, /* return type: void */
ctypes.uint32_t); /* parameter: uint32 */
wmctrldll.h
#ifdef WMCTRLDLL_EXPORTS
#define WMCTRLDLL_API __declspec(dllexport)
#else
#define WMCTRLDLL_API __declspec(dllimport)
#endif
WMCTRLDLL_API void wmctrl_window_focus (unsigned long wid);
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title);
wmctrldll.cpp
typedef struct {
HWND hWnd;
char title[255];
} myWinSpec;
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
char String[255];
myWinSpec* to_find = (myWinSpec*) lParam;
// not a window
if (!hWnd) return TRUE;
// not visible
if (!IsWindowVisible(hWnd)) return TRUE;
// no window title
if (!GetWindowTextA(hWnd, (LPSTR)String, 255)) return TRUE;
// no title match
if (strcmp(String, to_find->title) != 0) return TRUE;
to_find->hWnd = hWnd;
return FALSE;
}
WMCTRLDLL_API void wmctrl_window_focus(unsigned long wid) {
SetForegroundWindow((HWND) wid);
}
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title) {
myWinSpec to_find;
sprintf_s(to_find.title, sizeof(to_find.title), "%s", find_title);
to_find.hWnd = 0;
EnumWindows(EnumWindowsProc, (LPARAM)&to_find);
return (unsigned long) to_find.hWnd;
}
I found a reasonable workaround for this using web Notifications.
As mentioned in some of the other answers here there are some constraints with browsers and the OS where window.opener.focus() may not work.
I was able to get this to work using postMessage and Notifications.
To try this out:
From the parent (opener) tab add a 'message' event listener that creates a notification with a click listener:
window.addEventListener("message", (event) => {
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// Let's check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
});
}
});
Then from the child tab you need to use postMessage when focus back to parent tab is needed:
// Called from child tab
window.opener.postMessage(
{
message: "focus",
},
"*" // cross-origin
);
Once you call postMessage you should see a OS Web notification. If a user clicks this they should be redirected to the parent (opener) tab.
Workaround
In main window added script function:
function here() {
alert('Welcome Back') // seems needed to wake up document
window.focus()
}
In opened window invoke script function:
function HomeTab() {
O = window.opener;
if (O)
if (O.closed) alert('Home page has been closed')
else O.here()
else alert('This tab has no home page')
}
Works widely differently in different browsers
Some will have the parent tab blink
Some mark the parent tab, and you have to notice it
Some you have to click on home tab the first time and you can then give it permission to go directly to home tab without a confirm box.
I can't seem to get this to work.
In response to a click, window A opens window B (which then has focus). Then, in response to a click on B, the window calls window.opener.focus(), but the focus does not go back to A.
I have found a strange, strange workaround for Chrome (29, possibly others).
If I run:
window.opener.name = 'somename';
window.open(window.opener.location.href, window.opener.name);
window.opener.focus();
it does work (and doesn't reload window A). But this doesn't work for Firefox, and it is probably a fluke anyway.
It seems very clear to me what opener and focus are supposed to do, but window.opener.focus() doesn't work. What am I missing?
From the fine manual:
Makes a request to bring the window to the front. It may fail due to user settings and the window isn't guaranteed to be frontmost before this method returns.
Emphasis mine. Calling focus() is just a request and the browser is free to ignore you and you should generally expect to be ignored. Consider what sorts of nefarious things you could get up to by switching focus to a tiny window while someone is typing if you need some reasons why a browser would ignore your request.
If you need focus() to work for your application to work then you need to redesign your application so that it doesn't need to call focus().
I can see why a browser/OS will not allow a child windows to take over the focus (abuse of power). Here is a workaround:
In the parent window, declare a function in "window.external" that will trigger Javascript "alert()" or "confirm()".
Invoke that function from the child window.
The browser might ignore a request from a child window that wants to control the focus (e.g. window.opener.focus()), but the browser should honor a request from a parent window that triggers an alert() or a confirm() action, which requires to focus on the parent window.
JS Parent:
var child = window.open('child.html', 'child');
window.external.comeback = function() {
var back = confirm('Are you sure you want to comback?');
if(back) {
child.close();
} else {
child.focus();
}
}
JS Child:
// assuming you have jQuery
$('.btn').click() {
window.opener.external.comeback();
};
--I am using this code in a real world application to handle a checkout request that runs in child window, and I need to gracefully return to the parent window.
Web-facing or private intranet?
Window management is up to the Browser and the OS. HTML & ECMAscript have nothing to say about it.
If this is for a public-facing website, then just don't bother -- as they say, "Don't break the web."
But I really wanna!
If this is for some tightly managed (say Intranet) application of some kind then you'll need to resort to writing Addons/Extensions. It's certaintly easier if you can restrict yourself to a single browser & platform.
EDIT: Example for Firefox on Win32...
This solution works as a custom addon for Firefox which uses jsctypes internally to load a Win32 DLL. The window_focus() JavaScript function is exposed which does what you want.
There are 3 parts to this solution:
The privileged JavaScript code to load/bind the Win32 APIs
The CPP header file for our external DLL
The CPP source file for our external DLL
I built a simple GUI DLL project in MSVC++ with the later two files & compiled wmctrl.dll, depending on msvcr100.dll, and used Dependency Walker to find the "plain C" symbols exported by the DLL for use by js-ctypes. E.g: ?wmctrl_find_window##YAKPAD#Z is the "plain C" symbol for the C++ api function called wmctrl_find_window.
As a caveat, this code relies on temporarily being able to change the title of the window that needs to be focused so that Win32 APIs can examine all windows on your desktop to find the correct Firefox window.
You need to have access to privileged Mozilla platform APIs, i.e: JavaScript inside a Firefox Addon.
In your privileged JavaScript code:
// get API constants (might already be available)
const {Cc,Ci,Cu} = require("chrome");
// import js-ctypes
var file=null, lib=null, ctypes = {};
Cu.import("resource://gre/modules/ctypes.jsm", ctypes);
var ctypes = ctypes.ctypes;
// build platform specific library path
var filename = ctypes.libraryName("wmctrl"); // automatically adds '.dll'
var comp = "#mozilla.org/file/directory_service;1";
var file = Cc[comp].getService(Ci.nsIProperties).get("CurProcD", Ci.nsIFile);
file.append("browser_code"); // or whereever you put your DLL
file.append(filename);
// get the JavaScript library interface (load the library)
var lib = ctypes.open(file.path);
// wmctrl_find_window: returing unsigned 32bit (long) "window handle"
// takes string "window title".
var find_window = lib.declare(
"?wmctrl_find_window##YAKPAD#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.uint32_t, /* return type: uint32 */
ctypes.char.ptr); /* parameter: string */
// wmctrl_window_focus: takes unsigned 32bit (long) "window handle".
var window_focus = lib.declare(
"?wmctrl_window_focus##YAXK#Z", /* plain "C" DLL symbol */
ctypes.stdcall_abi, ctypes.void_t, /* return type: void */
ctypes.uint32_t); /* parameter: uint32 */
wmctrldll.h
#ifdef WMCTRLDLL_EXPORTS
#define WMCTRLDLL_API __declspec(dllexport)
#else
#define WMCTRLDLL_API __declspec(dllimport)
#endif
WMCTRLDLL_API void wmctrl_window_focus (unsigned long wid);
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title);
wmctrldll.cpp
typedef struct {
HWND hWnd;
char title[255];
} myWinSpec;
BOOL CALLBACK EnumWindowsProc(HWND hWnd, LPARAM lParam) {
char String[255];
myWinSpec* to_find = (myWinSpec*) lParam;
// not a window
if (!hWnd) return TRUE;
// not visible
if (!IsWindowVisible(hWnd)) return TRUE;
// no window title
if (!GetWindowTextA(hWnd, (LPSTR)String, 255)) return TRUE;
// no title match
if (strcmp(String, to_find->title) != 0) return TRUE;
to_find->hWnd = hWnd;
return FALSE;
}
WMCTRLDLL_API void wmctrl_window_focus(unsigned long wid) {
SetForegroundWindow((HWND) wid);
}
WMCTRLDLL_API unsigned long wmctrl_find_window(char* find_title) {
myWinSpec to_find;
sprintf_s(to_find.title, sizeof(to_find.title), "%s", find_title);
to_find.hWnd = 0;
EnumWindows(EnumWindowsProc, (LPARAM)&to_find);
return (unsigned long) to_find.hWnd;
}
I found a reasonable workaround for this using web Notifications.
As mentioned in some of the other answers here there are some constraints with browsers and the OS where window.opener.focus() may not work.
I was able to get this to work using postMessage and Notifications.
To try this out:
From the parent (opener) tab add a 'message' event listener that creates a notification with a click listener:
window.addEventListener("message", (event) => {
if (!("Notification" in window)) {
alert("This browser does not support desktop notification");
}
// Let's check whether notification permissions have already been granted
else if (Notification.permission === "granted") {
// If it's okay let's create a notification
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
// Otherwise, we need to ask the user for permission
else if (Notification.permission !== "denied") {
Notification.requestPermission().then(function (permission) {
// If the user accepts, let's create a notification
if (permission === "granted") {
var notification = new Notification("The opener needs your attention");
// Add click listener that will perform a window focus
notification.onclick = function (x) {
window.focus();
this.close();
};
}
});
}
});
Then from the child tab you need to use postMessage when focus back to parent tab is needed:
// Called from child tab
window.opener.postMessage(
{
message: "focus",
},
"*" // cross-origin
);
Once you call postMessage you should see a OS Web notification. If a user clicks this they should be redirected to the parent (opener) tab.
Workaround
In main window added script function:
function here() {
alert('Welcome Back') // seems needed to wake up document
window.focus()
}
In opened window invoke script function:
function HomeTab() {
O = window.opener;
if (O)
if (O.closed) alert('Home page has been closed')
else O.here()
else alert('This tab has no home page')
}
Works widely differently in different browsers
Some will have the parent tab blink
Some mark the parent tab, and you have to notice it
Some you have to click on home tab the first time and you can then give it permission to go directly to home tab without a confirm box.
I've implemented what seems to be the only way of communicating from javascript to objective-c on iOS using the UIWebView delegate shouldStartLoadWithRequest() method.
It seemed to work fine at first, but now I notice that if I make multiple calls from javascript to objective-c within a short period of time, the second call is usually ignored (The application is a piano keyboard, each keypress triggers a call to native code, when dealing with multiple touches the native code doesn't get called for every finger).
This is my objective-c code to respond to javascript calls. It's pretty derpy I know but I just wanted something that works for now.
- (BOOL)webView:(UIWebView *)webView2 shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType
{
// Intercept custom location change, URL begins with "js-call:"
NSString * requestString = [[request URL] absoluteString];
if ([requestString hasPrefix:#"js-call:"])
{
// Extract the selector name from the URL
NSArray * components = [requestString componentsSeparatedByString:#":"];
NSString * functionCall = [components objectAtIndex:1];
NSArray * params = [functionCall componentsSeparatedByString:#"%20"];
NSString * functionName = [params objectAtIndex:0];
// Parse playnote event
if ([functionName isEqualToString:#"playNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [m_audioController playNote:[param intValue]];
NSString * jscall = [NSString stringWithFormat:#"document.PlayNoteCallback(%i);", note_id];
NSLog(#"playNote: %i", (int)note_id);
[m_webView stringByEvaluatingJavaScriptFromString:jscall];
}
// Parse stopnote event
if ([functionName isEqualToString:#"stopNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [param intValue];
NSLog(#"stopNote: %i", (int)note_id);
[m_audioController stopNote:note_id];
}
// Parse log event
if ([functionName isEqualToString:#"debugLog"])
{
NSString * str = [requestString stringByReplacingOccurrencesOfString:#"%20" withString:#" "];
NSLog(#"%s", [str cStringUsingEncoding:NSStringEncodingConversionAllowLossy]);
}
// Cancel the location change
return NO;
}
// Accept this location change
return YES;
}
From javascript I call objective-c methods by setting the src attribute of a single hidden iframe. This will trigger the delegate method in objective-c and then the desired native code will get called.
$("#app_handle").attr("src", "js-call:playNote " + key.data("pitch"));
app_handle is the id of the iframe in question.
To sum up, the basics of my method work, but multiple calls within a short period of time do not work. Is this just an artifact of the terrible method in which we are forced to communicate from javascript to objective-c? Or am I doing something wrong? I know PhoneGap does something similar to achieve the same goal. I'd rather not use PhoneGap, so if they don't have this problem then I'd love to figure out what they are doing to make this work.
Update:
I just found this: send a notification from javascript in UIWebView to ObjectiveC
Which confirms my suspicions about calls made in quick succession getting lost. Apparently I need to either lump my calls together or manually delay the calls so that the url request has returned by the time I make another call.
The accepted answer does not solve the problem since location changes that arrive before the first is handled are still ignored. See the first comment.
I suggest the following approach:
function execute(url)
{
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", url);
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
}
You call the execute function repeatedly and since each call executes in its own iframe, they should not be ignored when called quickly.
Credits to this guy.
Rather than queuing everything on the JavaScript side, it's probably much easier and faster to move your complex logic (like the calls to the audio handler) off of the main thread using GCD. You can use dispatch_async() to queue up your events. If you put them into a serial queue, then they'll be certain to run in order, but you'll get back to the javascript faster. For instance:
Create a queue for your object during initialization:
self.queue = dispatch_queue_create("player", NULL);
In your callback:
if ([functionName isEqualToString:#"stopNote"])
{
NSString * param = [params objectAtIndex:1];
NoteInstanceID note_id = [param intValue];
NSLog(#"stopNote: %i", (int)note_id);
dispatch_async(self.queue, ^{[m_audioController stopNote:note_id]});
}
I'm trying to find a way to get if the browser is currently busy from JavaScript. I'm looking at making a Firefox extension to inject a Boolean value or something if the current page is loading something (either through ajax or just normal page loads), or the same with a Greasemonkey script, or through some JavaScript API (this would be the best solution, but from what I can see, nothing of the sort exists).
I was wondering what the best way to do this would be. I've been looking for Firefox Addon / Greasemonkey tutorials for making something like this and can't find anything. Does anyone have any tips or resources they could point me towards or better solutions for solving this?
Thanks
Edit: and by busy, I mostly just need to know if the browser is sending or receiving data from a server.
jQuery, a great javascript framework for DOM manipulation and performing ajax calls, provides two great hooks for determining when ajax calls are in progress:
$.ajaxStart() and $.ajaxStop()
Both of these hooks take a handler function that will be called when an ajax call is about to start, and when all ajax calls have ceased, respectively. These functions can be bound to any element on the page. You could set a global boolean value in your $.ajaxStart() handler to true and set it back to false in your $.ajaxStop() handler.
You could then check that boolean flag and determine whether ajax calls are in progress.
Something along these lines:
$(document).ajaxStart(function() {
window.ajaxBusy = true;
});
$(document).ajaxStop(function() {
window.ajaxBusy = false;
});
As far as determining when the browser is loading the current page, you could check
document.readyState. It returns a string of "loading" while the document is loading and a string of "complete" once it has loaded. You can bind a handler to document.onreadystatechange and set a global boolean that will indicate whether the document is still loading or not.
Something like this:
document.onreadystatechange = function() {
switch (document.readyState) {
case "loading":
window.documentLoading = true;
break;
case "complete":
window.documentLoading = false;
break;
default:
window.documentLoading = false;
}
}
EDIT:
It appears that $.ajaxStart() and $.ajaxStop() do NOT work for ajax calls invoked without jQuery. All XMLhttprequest objects have an event called readystatechange that you can attach a handler to. You could utilize this functionality to determine whether or not that individual call is done. You could push all references to outstanding calls onto an array, and in a setInterval() check that array's length. If it > 1, there are out standing ajax calls. It's a rough approach, and only one way of getting about it. There are probably other ways to do this. But here's the general approach:
// declare array to hold references to outstanding requets
window.orequets = [];
var req = XMLHttpRequest();
// open the request and send it here....
// then attach a handler to `onreadystatechange`
req.onreadystatechange = function() {
if (req.readyState != 4 || req.readyState != 3) {
// req is still in progress
orequests.push(req);
window.reqPos = orequests.length -1
} else {
window.orequests = orequests.slice(reqPos, reqPos + 1);
}
}
Do the above for each XMLHttpRequest() you will be sending, of course changing the request name for each one. Then run a setInterval() that runs every x amount of milliseconds, and checks the length property of orequests. If it is equal to zero, no requests are happening, if it is greater than zero, requests are still happening. If no requests are happening, you can either clear the interval through clearInterval() or keep it running.
Your setInterval might look something like this:
var ajaxInterval = setInterval(function() {
if (orequests.length > 0) {
// ajax calls are in progress
window.xttpBusy = true;
} else {
// ajax calls have ceased
window.xttpBusy = false;
// you could call clearInterval(ajaxInterval) here but I don't know if that's your intention
},
3000 // run every 3 seconds. (You can decide how often you want to run it)
});
Here's what I think I'll end up doing. This solution is like the one Alex suggested with the Jquery events, except that it works with anything that uses the XMLHttpRequest (Including Jquery):
var runningAjaxCount = 0;
var oldSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function() {
oldOnReady = this.onreadystatechange;
this.onreadystatechange = function() {
oldOnReady.call(this);
if(this.readyState == XMLHttpRequest.DONE) {
ajaxStopped();
}
}
ajaxStarted();
oldSend.apply(this, arguments);
}
function ajaxStarted() {
runningAjaxCount++;
}
function ajaxStopped() {
runningAjaxCount--;
}
function isCallingAjax() {
return runningAjaxCount > 0;
}
function isBrowserBusy() {
return document.readyState != "complete" && isCallingAjax();
}
The browser technically isn't ever "busy". Business is a very subjective term. Let's assume that the main thread is performing a simple while loop which blocks execution. This could be considered busy, but what if you have something like this:
function busy() {setTimeout(busy, 0);do_something();}
busy();
The browser isn't being blocked (per se), so whether or not the page is "busy" is very unclear. Also, that doesn't even begin to touch on web workers and code in the chrome.
You're going to be hard-pressed to do this, and even if you do, it likely won't work how you expect it to. Good luck, nonetheless.
I decided I would change how I implement printing in my Cocoa app by having my app provide string-data to a webpage in an embedded webview, and then I would print that webview/frame.
The problem is that my code isn't being called and I don't see an error being returned.
Here is the setup:
1) Use Dashcode and build a webpage. There is no "form" container in the generated document, but it has fields like this:
<input id="customerNameField" type="text" name="" value="">
<input id="customerStreetField" type="text" name="" value="">
2) In IB, I create a window, toss in a WebView, link it to an outlet in my controller and create an NSURLRequest, grab my WebView's mainFrame and have it load the request. That works, the page is displayed in the WebView. Code:
[[wv mainFrame] stopLoading];
request = [NSURLRequest requestWithURL:currentPrintTemplateURL];
[[wv mainFrame] loadRequest:request];
3) I want to set the value of the customerNameField in my webView, so I do the following:
3A) Use a class-method on my controller that allows all keys to be accessible to the bridge:
+(BOOL)isKeyExcludedFromWebScript:(const char *)name {
// TODO: Implement specific blocks here; but for now let it all through
NSLog(#"Excluding nothing from WebScript");
return NO;
}
3B) Add a JavaScript function to my HTML file, which I want to call with arguments from Cocoa:
function populateRepairFields(repairCase) {
document.getElementById("customerNameField").value = repairCase;
}
(I know the javascript single-line-of-code works because I can add a button to my page, trigger the function in onClick and the code modifies the value of the customerNamefield.)
3C) Ask the webview for it's windowScriptObject, create an NSArray of arguments to be passed to the javascript function, and then execute them using the callWebScriptMethod method:
// grab the data we want to send to the javascript
NSString *customerFirstName = [self valueForKeyPath:#"currentRepairCase.customer.firstName"];
NSLog(#"(debug) Ensure we have a value - customerFirstName = %#", customerFirstName);
// build the array whose items will be passed as arguments to the javascript method
NSArray * args = [NSArray arrayWithObjects:customerFirstName, nil];
// get the windowScriptObject, then (debug) log the output so we can be sure it is not null
ws = [wv windowScriptObject];
NSLog(#"WebScriptObject = %#", ws);
//Then call the javascript method via the bridge, logging the output so we can see if there is an WSUndefined error returned (results are the same even if we don't wrap it in an NSLog)
NSLog(#"WS ERR: %#", [ws callWebScriptMethod:#"populateRepairFields" withArguments:args]);
4) All that. And .. nothing. I don't see the class method being called (3A), and don't get an WSUndefined or any other error message (3C), and I don't see an alert if I tried to add a javascript alert in my code.
I thought maybe I would need to setup a delegate for the WebView, but after checking the docs, I don't see a delegate requirement for WebScript. (I then connected all the delegate outlets to my controller, but that didn't help.)
Why is my code not seemingly being called? What's missing?
Thanks..
You can only use the WebScriptObject instance after the WebView instance is ready, and the WebView instance is not ready right after your call
[[wv mainFrame] loadRequest:request];
because the loading is done in the background thread automatically by WebKit.
Your code should work as is, as long as you call it shortly afterwards from the webview delegate, action methods, etc.