Opening links in phonegap's inAppBrowser - javascript

i'm having difficulties trying to open up links in phonegap's (cordova's) inAppBrowser.
I need to be able to make it so the IAB can open both local phonegap links (file:///android_assets/www/example.html) and online links (https://www.google.com for instance)
I have achieved this so far by creating an object in index.js which creates an initiate IAB object, and then for every call from there onwards I was thinking that I could just open that link.
I've found a number of problems, for example when I try to use window.location = some_url it will change the index.html page to that some_url and hence my attached JS no longer works.
I've tried injecting a window.location from my index.html's JS into the IAB document however that breaks down when trying to fetch a local file
My current method is causing a memory leak because it's continuously opening IAB objects which I'm then having difficulties closing because ...
Using the .close() method on an IAB object shows (according to Chrome's Remote Devices view) that it doesn't actually delete the IAB window but rather turns it into an about:new tab
Heres my current code for the JS which is called in index.js (im sorry i know there's a lot of redundant code!)
var interceptor = {
// library dictionary object of <K,V> pairs in the form of <current page, desired page>
// insert your k,v pairs here:
library: {
'login/signup.php': 'example.html', //test data
'calendar/view.php': 'example.html',
'course/view.php?id=10': 'example.html'
},
// origin dictionary
// dictionary for redirecting from the phonegap page to the moodle page
// must be explicit in your page names and avoid duplication
// e.g. use full path notation -> full/path/to/file.html not file.html
origin: {
'current/attempt': ['example1.html', 'example2.html', 'course/index.php'] //test data
},
history_stack: [],
// stack of all current windows
browser_windows: [],
is_back: 'false',
windows: 0,
redirect_flag: false,
first: true,
currentWindow: null,
currentLocalFile: null,
get_origin: function() {
return this.origin;
},
get_library: function() {
return this.library;
},
// interceptor constructor
initialize: function(old_ref = null, default_url = config.moodleURL, android = true) {
// interval for checking our back flag
// the scope changes when you enter the anonymous closure and this changes. this is a hack for that
var self = this;
var ref;
console.log('default url: ' + default_url);
// check for android
if ((android) && (self.windows < 1)) {
// self.browser_windows[0] = cordova.InAppBrowser.open(default_url, '_blank', 'location=no,zoom=no');
ref = cordova.InAppBrowser.open(default_url, '_blank', 'location=yes,zoom=no');
self.first = false;
}
// otherwise iOS
else if ((!android) && (self.windows < 1)) {
// self.browser_windows[0] = cordova.InAppBrowser.open(default_url, '_blank', 'location=no,zoom=no,suppressesIncrementalRendering');
ref = cordova.InAppBrowser.open(default_url, '_blank', 'location=no,zoom=no,suppressesIncrementalRendering');
// self.browser_windows.push(ref);
self.first = false;
} else {
old_ref =
// old_ref = cordova.InAppBrowser.open(default_url, '_blank', 'location=yes,zoom=no');
ref = old_ref;
}
self.windows++;
var library_dictionary = this.get_library();
var origin_dictionary = this.get_origin();
var redirect_URL;
ref.addEventListener('loadstart', function(event) {
// push current page to histoy stack
self.history_stack.push(event.url);
// check to see if an element of one of our origin arrays is in the URL
for (var origin_list in origin_dictionary) {
if (event.url.includes(origin_list)) self.redirect_flag = true;
for (var elements in origin_dictionary[origin_list]) {
// if it is raise a flag and store the key that its array maps to
if ((event.url.includes(origin_dictionary[origin_list][elements])) && (self.redirect_flag)) {
redirect_URL = origin_list;
self.redirect_flag = false;
var temp_previous_pages = self.history_stack;
var temp_element;
// pop elements of stack until empty
while (temp_previous_pages.length !== 0) {
temp_element = temp_previous_pages.pop();
// if we find an element in the stack (our URL history) that matches the key that our array mapped to
if (temp_element.indexOf(redirect_URL) !== -1) {
// redirect and break from loop
self.initialize(ref, temp_element, android);
break;
}
}
}
}
}
for (var key in library_dictionary) {
if (event.url.includes(key)) {
self.initialize(ref, library_dictionary[key], android);
break;
}
}
});
ref.addEventListener('loadstop', function() {
// when we've finished loading our page set up an interval every 2.5 seconds to check
// if we've been signalled to change pages
console.log('here');
var is_back_interval = setInterval(function() {
ref.executeScript({
code: "localStorage.getItem('is_back')"
}, function(values) {
if (values[0] === 'true') {
// if we have been signalled then remove that signal from our html or moodle page
ref.executeScript({
code: "localStorage.setItem('is_back', '')"
});
// get 3rd last since the last will be the current page as will the 2nd last
prev_page = self.history_stack[self.history_stack.length - 3];
self.initialize(ref, prev_page, android);
}
});
}, 2500);
});
}
};
and heres index.js
var app = {
// Application Constructor
initialize: function() {
this.bindEvents();
},
// Bind Event Listeners
//
// Bind any events that are required on startup. Common events are:
// 'load', 'deviceready', 'offline', and 'online'.
bindEvents: function() {
document.addEventListener('deviceready', this.onDeviceReady, false);
},
// deviceready Event Handler
//
// The scope of 'this' is the event. In order to call the 'receivedEvent'
// function, we must explicitly call 'app.receivedEvent(...);'
onDeviceReady: function() {
app.receivedEvent('deviceready');
},
// Update DOM on a Received Event
receivedEvent: function(id) {
if (window.device.platform === "iOS"){
interceptor.initialize(config.moodleURL, false);
} else {
interceptor.initialize();
}
}
};
example.html: https://gist.github.com/anonymous/28a78ab0879878d7a9dac8eb89544cda

Related

Angularjs data binding issue / javascript being weird

This is undoubtedly a stupid problem where I'm just doing something simple wrong.
I have a page with several directives, loading their templates and controllers. All of which is working fine except for this one.
Using the controller as model, this. is the same as $scope.. So in my controller I have:
var self = this;
this.states = { showControls: false, showVideo: false }
this.showVideo = function() { self.states.showVideo = true; }
this.showControls = function() { self.states.showControls = true; }
$scope.$on(Constants.EVENT.START_WEBCAM, self.showVideo)
$scope.$on(Constants.EVENT.VIDEO_SUCCESS, self.showControls)
In the view I have a button to reveal this part of the view and subsequently request access to your webcam. Clicking the button broadcasts an event with $rootScope.$broadcast from the parent controller.
When the user grants access to the webcam (handled in the directive's link function) it broadcasts another event the same way.
Both methods are triggered by listening with $scope.$on, and both methods fire as they should. However, the showVideo method successfully updates its associated state property, and the showControls method does not. What am I doing wrong?
Using the debug tool it looks like states.showControls is being set to true, but this change isn't reflected in the view, and adding a watcher to the states object doesn't detect any change at this point either. It does when I set showVideo.
EDIT
This part is in the directive:
if (Modernizr && Modernizr.prefixed('getUserMedia', navigator)) {
userMedia = Modernizr.prefixed('getUserMedia', navigator);
}
var videoSuccess = function(stream) {
// Do some stuff
$rootScope.$broadcast(Constants.EVENT.VIDEO_SUCCESS);
}
scope.$on(Constants.EVENT.START_WEBCAM, function() {
if (MediaStreamTrack && MediaStreamTrack.getSources) {
MediaStreamTrack.getSources(function(sourceInfo) {
var audio = null;
var video = null;
_.each(sourceInfo, function(info, i) {
if (info.kind === "audio") {
audio = info.id;
} else if (info.kind === "video") {
video = info.id;
} else {
console.log("random unknown source: ", info);
}
});
if (userMedia) { userMedia(getReqs(), videoSuccess, error); }
});
}
});

HTML5 history API: cannot go backwards more than once

I have been trying to get my script working but apparently there is something wrong with it: when I try to go backwards with the browser back button, it stops at the first page backwards i.e. the second time I click the back button, does not work properly and instead updates the current page with itself.
Examples:
homepage -> second page -> third page -> second page -> second page -> second page (and so on)
homepage -> second page -> third page -> fourth page -> third page-> third page (and so on)
This instead works:
homepage -> second page -> homepage
Does anyone have a clue to what I am missing?
var domain = 'http://example.com/';
function updatePage(json){
var postData = JSON.parse(json);
// pushState
var url = domain + postData.url;
var title = postData.title;
document.title = title;
history.pushState({"page": url}, title, url);
// Clears some elements and fills them with the new content
// ...
// Creates an 'a' element that triggers AJAX for the next post
var a = document.createElement('a');
a.innerHTML = postData.next;
a.href = domain + postData.next;
document.getElementById('container').appendChild( a );
listenerAttacher( a );
// Creates another 'a' element that triggers AJAX for the previous post
a = document.createElement('a');
a.innerHTML = postData.previous;
a.href = domain + postData.previous;
document.getElementById('container').appendChild( a );
listenerAttacher( a );
}
function loadPost( resource ){
// Loads post data through AJAX using a custom function
loadHTML( resource, function(){
updatePage( this.responseText );
});
}
function listenerAttacher( element ){
// Adds a click listener to an element.
element.addEventListener('click', function(e){
e.preventDefault();
e.stopPropagation();
loadPost( this.href +'.json' );
return false;
},
false);
}
(function(){
history.replaceState({'page': window.location.href}, null, window.location.href);
// Adds the event listener to all elements that require it.
var titles = document.querySelectorAll('.post-title');
for (var i = 0; i < titles.length; i++){
listenerAttacher( titles[i] );
}
// Adds a popstate listener
window.addEventListener('popstate', function(e){
if ( e.state == null || e.state.page == domain ){
window.location = domain;
}
else {
loadPost( e.state.page + '.json' );
}
}, false);
})();
When you pressed back button, popstate event is fired and loadPost function is called. However in loadPost, history.pushState method is called again, which pushes the current page on the history stack again. Which explains why the first back button works and then it does not.
1) A quick fix is to check if the current state matches the state you are trying to push:
if (!history.state || history.state.page!=url)
history.pushState({ "page": url }, title, url);
2) Event better, you can add parameter to loadPost and updatePage functions to prevent unnecessary pushState calls:
function updatePage(json, disablePushState) {
...
// disablePushState is true when back button is pressed
// undefined otherwise
if (!disablePushState)
history.pushState({ "page": url }, title, url);
...
}
function loadPost(resource, disablePushState) {
// Loads post data through AJAX using a custom function
loadHTML(resource, function (responseText) {
updatePage(responseText, disablePushState);
});
}
...
window.addEventListener('popstate', function (e) {
if (e.state == null || e.state.page == domain) {
window.location = domain;
}
else {
loadPost(e.state.page + '.json', true);
}
return true;
});
Hope this help.

Universal button handler in javascript

I have a lot of buttons on my web app that request and post data to PHP to retrieve and update a database. I am struggling to create a universal way to prevent multiple button clicks when submitting forms, because I am using AJAX and Jquery.
This is my current implementation but I can't even tell if it works. It seems to work 99% of the time.
In my common functions.js file I have this function which is in the global scope
var canClick = true;
function buttonWithPromise(promise){
if(!canClick) return;
canClick = false;
promise.done(function(){
canClick = true;
});
}
Then any time I attach a .click to a dom element I do it like this:
$('body').on('click', '.table > .row', function(){
var nbr = $(this).attr('nbr');
buttonWithPromise(get_count(nbr));
});
And some function that might be called will have a deferred object.
function get_count(){
var defer = $.Deferred();
var options = "getCount"
Query.init(options)
.fetchData(function(data){ //Ajax data request
if(data){
}
defer.resolve();
});
return defer.promise();
}
Since this only sometimes works, I can tell it's wrong. Any advice for improvements?
Everything in Javascript is an object, yes? So why not:
$('body').on('click', '.button', function()
{
// Set default value of property
if(typeof this.isClicked === 'undefined')
this.isClicked = false;
// Check if button is working
if(this.isClicked)
{
// Send error to console if button is busy
console.log('Cannot click as a network action is occuring!');
}else
{
// Begin new network action if button is not busy
var self = this;
console.log('Begin network for: ' + $(this).text());
this.isClicked = true;
setTimeout(function()
{
// Reset button state once network action is done
console.log('End network for: ' + $(self).text());
self.isClicked = false;
//Call any callbacks/promises here
}, 5000);
}
});
Fiddle:
http://jsfiddle.net/mdLfug1t/
NOTE: I'm using setTimeout to simulate an ajax request
EDIT: Let me put this more into context:
function buttonWithPromise(promise)
{
if(typeof promise.canClick === 'undefined')
promise.canClick = true;
if(!promise.canClick) return;
promise.canClick = false;
promise.done(function()
{
promise.canClick = true;
});
}
The problem that you're running into is that "canClick" is global and so gets modified by every promise. You need to make it a property of a promise so that you can create infinite promises, each with their own instance of canClick.

Firefox add-on pageshow event fires before worker able to receive messages

I'm writing a firefox plugin and keeping track of each page's workers in an array. Apart from a bit of fancy footwork required to manage this array (as described here https://bugzilla.mozilla.org/show_bug.cgi?id=686035 and here Addon SDK - context-menu and page-mod workers) everything is working properly. One issue I'm having is that when listening to the tabs pageshow event (or the worker's own pageshow event for that matter), the callback seems to fire before the worker is actually ready. When retrieving the page's corresponding worker in the callback and using it to try to send a message to the content script, I'm receiving the error The page is currently hidden and can no longer be used until it is visible again. Normally, I'd just use a setTimeout and grit my teeth, but this isn't available for add-ons. What's a suitable workaround? The code for the main part of the add-on is below:
var { ToggleButton } = require('sdk/ui/button/toggle');
var panels = require('sdk/panel');
var tabs = require('sdk/tabs');
var self = require('sdk/self');
var pageMods = require('sdk/page-mod');
var ss = require('sdk/simple-storage');
var workers = [];
ss.storage.isPluginActive = ss.storage.isPluginActive || false;
var button = ToggleButton({
id: 'tomorrowww',
label: 'Tomorowww',
icon: {
'16': './icon-16.png',
'32': './icon-32.png',
'64': './icon-64.png'
},
onChange: handleButtonChange
});
var panel = panels.Panel({
contentURL: self.data.url('panel.html'),
contentScriptFile: self.data.url('panel-script.js'),
onHide: handlePanelHide,
width: 342,
height: 270
});
panel.port.on('panel-ready', handlePanelReady);
panel.port.on('plugin-toggled', handlePluginToggled);
panel.port.on('link-clicked', handleLinkClicked);
pageMods.PageMod({
include: ['*'],
contentScriptFile: [self.data.url('CancerDOMManager.js'), self.data.url('content-script.js')],
contentStyleFile: self.data.url('content-style.css'),
onAttach: function (worker) {
addWorker(worker);
sendActiveState(ss.storage.isPluginActive);
}
});
// move between tabs
tabs.on('activate', function () {
sendActiveState();
});
// this actually fires before the worker's pageshow event so isn't useful as the workers array will be out of sync
//tabs.on('pageshow', function () {
// sendActiveState();
//});
function addWorker (worker) {
if(workers.indexOf(worker) > -1) {
return;
}
worker.on('detach', handleWorkerDetach);
worker.on('pageshow', handleWorkerShown);
worker.on('pagehide', handleWorkerHidden);
workers.push(worker);
}
function handleWorkerDetach () {
removeWorker(this, true);
}
function handleWorkerShown () {
addWorker(this);
// back / forward page history
// trying to send the state here will trigger the page hidden error
sendActiveState();
}
function handleWorkerHidden () {
removeWorker(this);
}
function removeWorker (worker, removeEvents) {
var index = workers.indexOf(worker);
removeEvents = removeEvents || false;
if(index > -1) {
if(removeEvents) {
worker.removeListener('detach', handleWorkerDetach);
worker.removeListener('pageshow', handleWorkerShown);
worker.removeListener('pagehide', handleWorkerHidden);
}
workers.splice(index, 1);
}
}
function getWorkersForCurrentTab () {
var i;
var tabWorkers = [];
i = workers.length;
while(--i > -1) {
if(workers[i].tab.id === tabs.activeTab.id) {
tabWorkers.push(workers[i]);
}
}
return tabWorkers;
}
function handlePanelReady () {
setActive(ss.storage.isPluginActive);
}
function setActive (bool) {
ss.storage.isPluginActive = bool;
panel.port.emit('active-changed', bool);
sendActiveState();
}
function sendActiveState () {
var tabWorkers = getWorkersForCurrentTab();
var i = tabWorkers.length;
while(--i > -1) {
tabWorkers[i].port.emit('toggle-plugin', ss.storage.isPluginActive);
}
}
function handleButtonChange (state) {
if(state.checked) {
panel.show({
position: button
});
}
}
function handlePanelHide () {
button.state('window', {checked: false});
}
function handleLinkClicked (url) {
if(panel.isShowing) {
panel.hide();
}
tabs.open(url);
}
function handlePluginToggled (bool) {
if(panel.isShowing) {
panel.hide();
}
setActive(bool);
}
try using contentScriptWhen: "start" in the page-mod
I was dealing with a similar problem. I think I have it working the way I want now by putting the listener in the content script instead of the addon script. I listen for the event on the window, I then emit a message from my content script to my addon script, my addon script then sends a message back to the content script with the information needed from the addon script.
In my code, I am working on update the preferences in the content script to ensure that the tab always has the most up to date settings when they are changed, only the addon script can listen to the prefs change event.
This particular snippet will listen for when the page is navigated to from history (i.e., back or forward button), will inform the addon script, the addon script will get the most up to date preferences, and then send them back to a port listening in the content script.
Content script:
window.onpageshow = function(){
console.log("onpageshow event fired (content script)");
self.port.emit("triggerPrefChange", '');
};
Addon Script (e.g., main.js:
worker.port.on("triggerPrefChange", function() {
console.log("Received request to triggerPrefChange in the addon script");
worker.port.emit("setPrefs", prefSet.prefs);
});
Since the event is being fired from the DOM event, the page must be shown. I am not sure if listening to the pageshow event in the addon script is doing what we think.

Tab independent jQuery on Firefox extension

I'm developping a Firefox based on jQuery as described in this Answer here.
After implementing the example provided in the answer, eveything works fine, but the problem is the code between Firefox Tabs is somehow linked, and example.doc always refers to the last opened tab.
Opened tab1 : the plugin-example has been added and to the current page.
this.doc refers to tab1.
Oepened tab2: the plugin-example has been added to to current page (tab2).
this.doc now refers to tab2
back to viewing tab1 : this.doc still refers to tab1.
clicking on plugin-example on tab1 will act on the plugin-example in tab2 instead.
How can I make my code independent between tabs?
Here is an excrept from the code:
(function() {
jQuery.noConflict();
$ = function(selector,context) {
return new jQuery.fn.init(selector,context||example.doc);
};
$.fn = $.prototype = jQuery.fn;
example = new function(){};
example.run = function(doc,aEvent) {
if (doc.getElementById("plugin-example")) return;
this.doc = doc;
this.main = main = $('<div id="plugin-example">').appendTo(doc.body).html('Example Loaded!');
this.main.click(function() { //<--- added this function
example.main.html(example.doc.location.href);
});
main.css({
background:'#FFF',color:'#000',position:'absolute',top:0,left:0,padding:8
});
};
// Bind Plugin
var delay = function(aEvent) {
var doc = aEvent.originalTarget; setTimeout(function() {
example.run(doc,aEvent);
}, 1);
};
var load = function() {
gBrowser.addEventListener("DOMContentLoaded", delay, true);
};
window.addEventListener("pageshow", load, false);
})();
Your code (overlay script) will only run once per window, not once per tab. So there is only one example instance per window. And hence example.doc will be set to whatever dispatched DOMContentLoaded last.
Your function should properly close over the document and avoid global state.
This is who I would write it (then again, I would avoid jquery (in add-ons) like the plague...)
// Use strict mode in particular to avoid implicitly var declarations
(function() {
"use strict";
// Main runner function for each content window.
// Similar to SDK page-mod, but without the security boundaries.
function run(window, document) {
// jquery setup. per https://stackoverflow.com/a/496970/484441
$ = function(selector,context) {
return new jq.fn.init(selector,context || document);
};
$.fn = $.prototype = jq.fn;
if (document.getElementById("my-example-addon-container")) {
return;
}
let main = $('<div id="my-example-addon-container">');
main.appendTo(document.body).text('Example Loaded!');
main.click(function() { //<--- added this function
main.text(document.location.href);
});
main.css({
background:'#FFF',color:'#000',position:'absolute',top:0,left:0,padding:8
});
};
const log = Components.utils.reportError.bind(Components.utils);
// Do not conflict with other add-ons using jquery.
const jq = jQuery.noConflict(true);
gBrowser.addEventListener("DOMContentLoaded", function load(evt) {
try {
// Call run with this == window ;)
let doc = evt.target.ownerDocument || evt.target;
if (!doc.location.href.startsWith("http")) {
// Do not even attempt to interact with non-http(s)? sites.
return;
}
run.call(doc.defaultView, doc.defaultView, doc);
}
catch (ex) {
log(ex);
}
}, true);
})();
Here is a complete add-on as a gist. Just drop in a copy of jquery and it should be good to go.
PS: Reposted this at in the jquery in extensions question

Categories