Skip to content

Commit

Permalink
Add support for backbone apps defined into iframes #24
Browse files Browse the repository at this point in the history
  • Loading branch information
Maluen committed Sep 7, 2014
1 parent 6071322 commit fecd5ae
Show file tree
Hide file tree
Showing 14 changed files with 212 additions and 55 deletions.
6 changes: 4 additions & 2 deletions js/backboneAgent/backboneAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,12 @@ Modules.set('backboneAgent', function() {

// business logic
initialize: function() {

reportController.start();

// detect backbone
backboneController.onBackboneDetected(u.bind(function(Backbone) {
this.isBackboneDetected = true;
debug.log('Backbone detected: ', Backbone);

// detect backbone components
Expand All @@ -42,6 +43,7 @@ Modules.set('backboneAgent', function() {
},

// PUBLIC API
isBackboneDetected: false,
appComponentsInfos: appComponentsInfos


Expand All @@ -50,4 +52,4 @@ Modules.set('backboneAgent', function() {
return backboneAgent;
});

window.__backboneAgent = Modules.get('backboneAgent');
window.__backboneAgent = Modules.get('backboneAgent');
5 changes: 3 additions & 2 deletions js/backboneAgent/controllers/backboneController.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ Modules.set('controllers.backboneController', function() {
// Calls the callback passing to it the Backbone object every time it's detected.
// The function uses multiple methods of detection.
onBackboneDetected: function(callback) {
var handleBackbone = function(Backbone) {
var handleBackbone = u.bind(function(Backbone) {
// skip if already detected
// (needed because the app could define Backbone in multiple ways at once)
if (hidden.get(Backbone, "isDetected")) return;
hidden.set(Backbone, "isDetected", true);

this.trigger('backboneDetected', Backbone);
callback(Backbone);
}
}, this);

// global
u.onSetted(window, "Backbone", handleBackbone);
Expand Down
32 changes: 19 additions & 13 deletions js/backboneAgent/controllers/reportController.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ Modules.set('controllers.reportController', function() {
// imports
var Component = Modules.get('Component');
var u = Modules.get('utils');
var backboneController = Modules.get('controllers.backboneController');
var appComponentsInfos = Modules.get('collections.appComponentsInfos');
var debug = Modules.get('debug');

Expand All @@ -10,34 +11,38 @@ Modules.set('controllers.reportController', function() {
start: function() {
// setup reports

backboneController.on('backboneDetected', u.bind(function(Backbone) {
this.sendReport('backboneDetected');
}, this));

u.each(appComponentsInfos, u.bind(function(appComponentsInfo) {

// reports about new app components
appComponentsInfo.on('add', u.bind(function(appComponent) {
this.sendReport(appComponentsInfo.category+":new", {
this.sendReport(appComponentsInfo.category+':new', {
componentIndex: appComponent.index
});
debug.log("New " + appComponentsInfo.category, appComponent);
debug.log('New ' + appComponentsInfo.category, appComponent);
}, this));

// reports about new app component actions
appComponentsInfo.on('actions:add', u.bind(function(appComponentAction) {
var appComponentIndex = appComponentAction.appComponentInfo.get('index');
this.sendReport(appComponentsInfo.category+":"+appComponentIndex+":action", {
this.sendReport(appComponentsInfo.category+':'+appComponentIndex+':action', {
componentActionIndex: appComponentAction.index
});
//debug.log("New action: ", appComponentAction);
//debug.log('New action: ', appComponentAction);
}, this));

// report about app component attribute changes
appComponentsInfo.on('change', u.bind(function(appComponentInfo) {
u.each(appComponentInfo.changed, function(attributeValue, attributeName) {
this.sendReport(appComponentsInfo.category+":"+appComponentInfo.index+":change", {
this.sendReport(appComponentsInfo.category+':'+appComponentInfo.index+':change', {
attribute: attributeName
});
// (we send only the attribute name for serialization and performance reasons)

//debug.log("Attribute " + attributeName + " of a " + appComponentInfo.category + " has changed: ", attributeValue);
//debug.log('Attribute ' + attributeName + ' of a ' + appComponentInfo.category + ' has changed: ', attributeValue);
}, this);
}, this));

Expand All @@ -47,18 +52,19 @@ Modules.set('controllers.reportController', function() {
// Note: name is prefixed by "backboneAgent:" and can't contain spaces
// (because it's transformed in a Backbone event in the Panel)
sendReport: function(name, report) {
report = report || {};
// the timestamp is tipicaly used by the panel to exclude old reports
report.timestamp = new Date().getTime();

this.sendPageMessage({
name: "backboneAgent:"+name,
data: report
});
this.sendPageMessage('backboneAgent:'+name, report);
},

sendPageMessage: function(message) {
message.target = "page"; // the message is about the inspected page
window.postMessage(message, "*");
sendPageMessage: function(name, data) {
window.postMessage({
target: 'page',
name: name,
data: data
}, '*');
}

}))();
Expand Down
32 changes: 22 additions & 10 deletions js/contentscript.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,30 @@
// Receives messages from the inspected page and redirects them to the background,
var frameURL = window.location.href;
var isTopFrame = (window.parent == window);

// Receives messages from the inspected page frame and redirects them to the background,
// building up the first step towards the communication between the backbone agent and the panel.
window.addEventListener("message", function(event) {
// We only accept messages from ourselves
// Only accept messages from same frame
if (event.source != window) return;

var message = event.data;

// Only accept our messages
if (typeof message != 'object' || message === null || message.target != 'page') return;

message.frameURL = frameURL;
chrome.extension.sendMessage(message);
}, false);

// Sends a message to the background when the DOM of the inspected page is ready
// (typically used by the panel to check if the backbone agent is on the page).
window.addEventListener('DOMContentLoaded', function() {
chrome.extension.sendMessage({
target: 'page',
name: 'ready'
});
}, false);
if (isTopFrame) {
/* Code to be executed only if this is the top frame content script! */

// Sends a message to the background when the DOM of the inspected page is ready
// (typically used by the panel to check if the backbone agent is on the page).
window.addEventListener('DOMContentLoaded', function() {
chrome.extension.sendMessage({
target: 'page',
name: 'ready'
});
}, false);
}
101 changes: 97 additions & 4 deletions js/panel/backboneAgentClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,119 @@ define(["backbone", "underscore", "inspectedPageClient"], function(Backbone, _,
var backboneAgentClient = new (function() {
_.extend(this, Backbone.Events);

this.context = "window.__backboneAgent";

this.initialize = function() {
_.bindAll(this);

// the frame url in which Backbone has been detected
this.frameURL = undefined;
};

// Call the callback passing to it a boolean indicating if the Backbone Agent is active.
this.isActive = function(callback) {
// the agent is injected into every frame,
// thus is sufficient to check if exists on one of them, like on the top frame.
// (the inspectedPageClient is used directly to be sure of executing the function
// on the top frame without having to pass its url after having detected Backbone)
inspectedPageClient.execFunction(function() {
// can't pass directly this.context as context and do "this !== undefined"
// since the code isn't executed in strict-mode, an undefined context would be
// transformed into the window object.
return (window.__backboneAgent !== undefined);
}, [], callback);
};

// Activate the Backbone Agent by reloading the inspected page and injecting it at
// the beginning.
// the beginning of each frame.
this.activate = function() {
inspectedPageClient.reloadInjecting(chrome.extension.getURL("js/backboneAgent"));
};

// Execute the passed function in the inspected page using the Backbone Agent as context.
this.execFunction = function(func, args, onExecuted) {
inspectedPageClient.execFunction(func, args, onExecuted, "window.__backboneAgent");
// Execute the function in the inspected page, using the Backbone agent as context.
// If the frameURL parameter is omitted, the default frame will be used (this.frameURL),
// thus is not be possible to execute functions in the top frame by passing undefined
// if the default value is not! Nevertheless, after having detected Backbone,
// this will allow by default to execute functions in the frame where Backbone was found.
this.execFunction = function(func, args, onExecuted, frameURL) {
if (!frameURL) frameURL = this.frameURL;
inspectedPageClient.execFunction(func, args, onExecuted, this.context, frameURL);
};

// Call the callback with the frame url in which Backbone has been detected
// or with null if not detected yet.
this.getBackboneFrame = function(callback) {

var process = _.bind(function(frame, callback) {
this.execFunction(function() {
return (window.__backboneAgent !== undefined) &&
window.__backboneAgent.isBackboneDetected;
}, [], callback, frame.url);
}, this);

var processAll = _.bind(function(frames, index) {
index = index || 0;

if (index >= frames.length) {
// not found
callback(null);
return;
}

// process current
process(frames[index], _.bind(function(isBackboneDetected) {
if (!isBackboneDetected) {
// process next
processAll(frames, index+1);
} else {
// found
callback(frames[index]);
}
}, this));
}, this);

inspectedPageClient.getFrames(processAll);
};

// Find the first backboneAgent instance in which Backbone has been detected (or wait for it)
// and save its frame url in order to execute code and receive messages only on/from it.
// onDetected is an optional function to be called when the detection is done.
this.detectBackbone = function(onDetected) {
// Detect if there is a frame with backbone, if not, wait for it.
// Since the backboneDetected event could arrive while checking the current frames,
// is necessary to listen for it since from the start.

// function to be called with the backbone frame that has been found
var onBackboneFrame = _.once(_.bind(function(frameURL) { // assures single execution
// if existing backbone was found, remove the pending listener for future backbone
this.stopListening.apply(this, backboneDetectedListener);

this.frameURL = frameURL; // execute code on backbone frame by default
this.listenTo(inspectedPageClient, "all", _.bind(function(name, data, frameURL) {
// re-trigger only events of the backbone agent instance
// that is in the backbone frame
var isValidMessage = frameURL == this.frameURL &&
name.indexOf("backboneAgent:") == 0; // starts with
if (isValidMessage) this.trigger(name, data);
}, this));

if (onDetected) onDetected();
}, this));

// wait for future backbone
var backboneDetectedListener = [
inspectedPageClient,
"backboneAgent:backboneDetected",
function(data, frameURL) {
onBackboneFrame(frameURL);
}
];
this.listenToOnce.apply(this, backboneDetectedListener);

// get (eventual) existing backbone
this.getBackboneFrame(_.bind(function(frame) {
if (frame) onBackboneFrame(frame.url);
}, this));
};

this.initialize();
Expand Down
6 changes: 3 additions & 3 deletions js/panel/collections/AppComponentActions.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* This collection is sorted in reverse order (latest first) */

define(["backbone", "underscore", "backboneAgentClient", "inspectedPageClient",
define(["backbone", "underscore", "backboneAgentClient",
"collections/Collection", "models/AppComponentAction", "setImmediate"],
function(Backbone, _, backboneAgentClient, inspectedPageClient, Collection, AppComponentAction, setImmediate) {
function(Backbone, _, backboneAgentClient, Collection, AppComponentAction, setImmediate) {

var AppComponentActions = Collection.extend({

Expand Down Expand Up @@ -48,7 +48,7 @@ function(Backbone, _, backboneAgentClient, inspectedPageClient, Collection, AppC
var reportName = "backboneAgent:"+this.component.category+":"
+ this.component.index+":action";

this.realTimeUpdateListener = [inspectedPageClient, reportName, _.bind(function(report) {
this.realTimeUpdateListener = [backboneAgentClient, reportName, _.bind(function(report) {
onNew(report.componentActionIndex, report.timestamp);
}, this)];

Expand Down
6 changes: 3 additions & 3 deletions js/panel/collections/AppComponents.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/* Collezione di componenti dell'applicazione di una data categoria.
E' il tipo padre di tutte le altre collezioni di componenti (di viste, modelli, etc.) */

define(["backbone", "underscore", "backboneAgentClient", "inspectedPageClient",
define(["backbone", "underscore", "backboneAgentClient",
"collections/Collection", "collections/AppComponentActions"],
function(Backbone, _, backboneAgentClient, inspectedPageClient, Collection, AppComponentActions) {
function(Backbone, _, backboneAgentClient, Collection, AppComponentActions) {

var AppComponents = Collection.extend({

Expand Down Expand Up @@ -34,7 +34,7 @@ function(Backbone, _, backboneAgentClient, inspectedPageClient, Collection, AppC
startRealTimeUpdateLogic: function(onNew) {
var reportName = "backboneAgent:"+this.componentCategory+":new";

this.realTimeUpdateListener = [inspectedPageClient, reportName, _.bind(function(report) {
this.realTimeUpdateListener = [backboneAgentClient, reportName, _.bind(function(report) {
onNew(report.componentIndex, report.timestamp);
}, this)];

Expand Down
Loading

0 comments on commit fecd5ae

Please sign in to comment.