Commit bdbcebe6 authored by Romain Courteaud's avatar Romain Courteaud

WIP automatic URL handler.

parent 914cbd3d
......@@ -666,7 +666,52 @@ if (typeof document.contains !== 'function') {
}
}
;/*! RenderJs */
/*global console*/
/*jslint nomen: true*/
function loopEventListener(target, type, useCapture, callback) {
"use strict";
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback,
callback_promise;
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === "function")) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
handle_event_callback = function (evt) {
evt.stopPropagation();
evt.preventDefault();
cancelResolver();
callback_promise = new RSVP.Queue()
.push(function () {
return callback(evt);
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
}
/*
* renderJs - Generic Gadget library renderer.
......@@ -741,6 +786,71 @@ if (typeof document.contains !== 'function') {
/////////////////////////////////////////////////////////////////
// Helper functions
/////////////////////////////////////////////////////////////////
function listenHashChange(gadget) {
function extractHashAndDispatch(evt) {
var hash = (evt.newURL || window.location.toString()).split('#')[1],
subhashes,
subhash,
keyvalue,
index,
options = {};
if (hash === undefined) {
hash = "";
} else {
hash = hash.split('?')[0];
}
function optionalize(key, value, dict) {
var key_list = key.split("."),
kk,
i;
for (i = 0; i < key_list.length; i += 1) {
kk = key_list[i];
if (i === key_list.length - 1) {
dict[kk] = value;
} else {
if (!dict.hasOwnProperty(kk)) {
dict[kk] = {};
}
dict = dict[kk];
}
}
}
subhashes = hash.split('&');
for (index in subhashes) {
if (subhashes.hasOwnProperty(index)) {
subhash = subhashes[index];
if (subhash !== '') {
keyvalue = subhash.split('=');
if (keyvalue.length === 2) {
optionalize(decodeURIComponent(keyvalue[0]),
decodeURIComponent(keyvalue[1]),
options);
}
}
}
}
if (gadget.render !== undefined) {
return gadget.render(options);
}
}
var result = loopEventListener(window, 'hashchange', false,
extractHashAndDispatch),
event = document.createEvent("Event");
event.initEvent('hashchange', true, true);
event.newURL = window.location.toString();
window.dispatchEvent(event);
return result;
}
function removeHash(url) {
var index = url.indexOf('#');
if (index > 0) {
......@@ -1070,6 +1180,8 @@ if (typeof document.contains !== 'function') {
};
RenderJSGadget.declareAcquiredMethod("aq_reportServiceError",
"reportServiceError");
RenderJSGadget.declareAcquiredMethod("aq_pleasePublishMyState",
"pleasePublishMyState");
/////////////////////////////////////////////////////////////////
// RenderJSGadget.allowPublicAcquisition
......@@ -1090,6 +1202,22 @@ if (typeof document.contains !== 'function') {
};
}
function pleasePublishMyState(param_list, child_gadget_scope) {
var new_param = {},
key;
for (key in this.state_parameter_dict) {
if (this.state_parameter_dict.hasOwnProperty(key)) {
new_param[key] = this.state_parameter_dict[key];
}
}
if (child_gadget_scope === undefined) {
throw new Error("gadget scope is mandatory");
}
new_param[child_gadget_scope] = param_list[0];
param_list = [new_param];
return this.aq_pleasePublishMyState.apply(this, param_list);
}
/////////////////////////////////////////////////////////////////
// RenderJSEmbeddedGadget
/////////////////////////////////////////////////////////////////
......@@ -1557,6 +1685,8 @@ if (typeof document.contains !== 'function') {
tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.__path = url;
tmp_constructor.prototype.__acquired_method_dict = {};
tmp_constructor.allowPublicAcquisition("pleasePublishMyState",
pleasePublishMyState);
// https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
// https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
......@@ -1667,6 +1797,39 @@ if (typeof document.contains !== 'function') {
// Bootstrap process. Register the self gadget.
///////////////////////////////////////////////////
function mergeSubDict(dict) {
var subkey,
subkey2,
subresult2,
value,
result = {};
for (subkey in dict) {
if (dict.hasOwnProperty(subkey)) {
value = dict[subkey];
if (value instanceof Object) {
subresult2 = mergeSubDict(value);
for (subkey2 in subresult2) {
if (subresult2.hasOwnProperty(subkey2)) {
// XXX key should not have an . inside
if (result.hasOwnProperty(subkey + "." + subkey2)) {
throw new Error("Key " + subkey + "." +
subkey2 + " already present");
}
result[subkey + "." + subkey2] = subresult2[subkey2];
}
}
} else {
if (result.hasOwnProperty(subkey)) {
throw new Error("Key " + subkey + " already present");
}
result[subkey] = value;
}
}
}
return result;
}
function bootstrap() {
var url = removeHash(window.location.href),
tmp_constructor,
......@@ -1693,6 +1856,26 @@ if (typeof document.contains !== 'function') {
},
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
},
pleaseRedirectMyHash: function (param_list) {
window.location.replace(param_list[0]);
},
pleasePublishMyState: function (param_list) {
var key,
first = true,
hash = "#";
param_list[0] = mergeSubDict(param_list[0]);
for (key in param_list[0]) {
if (param_list[0].hasOwnProperty(key)) {
if (!first) {
hash += "&";
}
hash += encodeURIComponent(key) + "=" +
encodeURIComponent(param_list[0][key]);
first = false;
}
}
return hash;
}
};
// Stop acquisition on the last acquisition gadget
......@@ -1734,6 +1917,10 @@ if (typeof document.contains !== 'function') {
// Create the root gadget instance and put it in the loading stack
root_gadget = new gadget_model_dict[url]();
tmp_constructor.declareService(function () {
return listenHashChange(this);
});
setAqParent(root_gadget, last_acquisition_gadget);
} else {
......@@ -1820,6 +2007,8 @@ if (typeof document.contains !== 'function') {
}
tmp_constructor.prototype.__acquired_method_dict = {};
tmp_constructor.allowPublicAcquisition("pleasePublishMyState",
pleasePublishMyState);
gadget_loading_klass = tmp_constructor;
function init() {
......@@ -1972,6 +2161,9 @@ if (typeof document.contains !== 'function') {
//we consider current gadget is parent gadget
//redifine last acquisition gadget
iframe_top_gadget = true;
tmp_constructor.declareService(function () {
return listenHashChange(this);
});
setAqParent(root_gadget, last_acquisition_gadget);
} else {
throw error;
......
This diff is collapsed.
/*! RenderJs */
/*global console*/
/*jslint nomen: true*/
function loopEventListener(target, type, useCapture, callback) {
"use strict";
//////////////////////////
// Infinite event listener (promise is never resolved)
// eventListener is removed when promise is cancelled/rejected
//////////////////////////
var handle_event_callback,
callback_promise;
function cancelResolver() {
if ((callback_promise !== undefined) &&
(typeof callback_promise.cancel === "function")) {
callback_promise.cancel();
}
}
function canceller() {
if (handle_event_callback !== undefined) {
target.removeEventListener(type, handle_event_callback, useCapture);
}
cancelResolver();
}
function itsANonResolvableTrap(resolve, reject) {
handle_event_callback = function (evt) {
evt.stopPropagation();
evt.preventDefault();
cancelResolver();
callback_promise = new RSVP.Queue()
.push(function () {
return callback(evt);
})
.push(undefined, function (error) {
if (!(error instanceof RSVP.CancellationError)) {
canceller();
reject(error);
}
});
};
target.addEventListener(type, handle_event_callback, useCapture);
}
return new RSVP.Promise(itsANonResolvableTrap, canceller);
}
/*
* renderJs - Generic Gadget library renderer.
......@@ -74,6 +119,71 @@
/////////////////////////////////////////////////////////////////
// Helper functions
/////////////////////////////////////////////////////////////////
function listenHashChange(gadget) {
function extractHashAndDispatch(evt) {
var hash = (evt.newURL || window.location.toString()).split('#')[1],
subhashes,
subhash,
keyvalue,
index,
options = {};
if (hash === undefined) {
hash = "";
} else {
hash = hash.split('?')[0];
}
function optionalize(key, value, dict) {
var key_list = key.split("."),
kk,
i;
for (i = 0; i < key_list.length; i += 1) {
kk = key_list[i];
if (i === key_list.length - 1) {
dict[kk] = value;
} else {
if (!dict.hasOwnProperty(kk)) {
dict[kk] = {};
}
dict = dict[kk];
}
}
}
subhashes = hash.split('&');
for (index in subhashes) {
if (subhashes.hasOwnProperty(index)) {
subhash = subhashes[index];
if (subhash !== '') {
keyvalue = subhash.split('=');
if (keyvalue.length === 2) {
optionalize(decodeURIComponent(keyvalue[0]),
decodeURIComponent(keyvalue[1]),
options);
}
}
}
}
if (gadget.render !== undefined) {
return gadget.render(options);
}
}
var result = loopEventListener(window, 'hashchange', false,
extractHashAndDispatch),
event = document.createEvent("Event");
event.initEvent('hashchange', true, true);
event.newURL = window.location.toString();
window.dispatchEvent(event);
return result;
}
function removeHash(url) {
var index = url.indexOf('#');
if (index > 0) {
......@@ -403,6 +513,8 @@
};
RenderJSGadget.declareAcquiredMethod("aq_reportServiceError",
"reportServiceError");
RenderJSGadget.declareAcquiredMethod("aq_pleasePublishMyState",
"pleasePublishMyState");
/////////////////////////////////////////////////////////////////
// RenderJSGadget.allowPublicAcquisition
......@@ -423,6 +535,22 @@
};
}
function pleasePublishMyState(param_list, child_gadget_scope) {
var new_param = {},
key;
for (key in this.state_parameter_dict) {
if (this.state_parameter_dict.hasOwnProperty(key)) {
new_param[key] = this.state_parameter_dict[key];
}
}
if (child_gadget_scope === undefined) {
throw new Error("gadget scope is mandatory");
}
new_param[child_gadget_scope] = param_list[0];
param_list = [new_param];
return this.aq_pleasePublishMyState.apply(this, param_list);
}
/////////////////////////////////////////////////////////////////
// RenderJSEmbeddedGadget
/////////////////////////////////////////////////////////////////
......@@ -890,6 +1018,8 @@
tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.__path = url;
tmp_constructor.prototype.__acquired_method_dict = {};
tmp_constructor.allowPublicAcquisition("pleasePublishMyState",
pleasePublishMyState);
// https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
// https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
......@@ -1000,6 +1130,39 @@
// Bootstrap process. Register the self gadget.
///////////////////////////////////////////////////
function mergeSubDict(dict) {
var subkey,
subkey2,
subresult2,
value,
result = {};
for (subkey in dict) {
if (dict.hasOwnProperty(subkey)) {
value = dict[subkey];
if (value instanceof Object) {
subresult2 = mergeSubDict(value);
for (subkey2 in subresult2) {
if (subresult2.hasOwnProperty(subkey2)) {
// XXX key should not have an . inside
if (result.hasOwnProperty(subkey + "." + subkey2)) {
throw new Error("Key " + subkey + "." +
subkey2 + " already present");
}
result[subkey + "." + subkey2] = subresult2[subkey2];
}
}
} else {
if (result.hasOwnProperty(subkey)) {
throw new Error("Key " + subkey + " already present");
}
result[subkey] = value;
}
}
}
return result;
}
function bootstrap() {
var url = removeHash(window.location.href),
tmp_constructor,
......@@ -1026,6 +1189,26 @@
},
reportServiceError: function (param_list) {
letsCrash(param_list[0]);
},
pleaseRedirectMyHash: function (param_list) {
window.location.replace(param_list[0]);
},
pleasePublishMyState: function (param_list) {
var key,
first = true,
hash = "#";
param_list[0] = mergeSubDict(param_list[0]);
for (key in param_list[0]) {
if (param_list[0].hasOwnProperty(key)) {
if (!first) {
hash += "&";
}
hash += encodeURIComponent(key) + "=" +
encodeURIComponent(param_list[0][key]);
first = false;
}
}
return hash;
}
};
// Stop acquisition on the last acquisition gadget
......@@ -1067,6 +1250,10 @@
// Create the root gadget instance and put it in the loading stack
root_gadget = new gadget_model_dict[url]();
tmp_constructor.declareService(function () {
return listenHashChange(this);
});
setAqParent(root_gadget, last_acquisition_gadget);
} else {
......@@ -1153,6 +1340,8 @@
}
tmp_constructor.prototype.__acquired_method_dict = {};
tmp_constructor.allowPublicAcquisition("pleasePublishMyState",
pleasePublishMyState);
gadget_loading_klass = tmp_constructor;
function init() {
......@@ -1305,6 +1494,9 @@
//we consider current gadget is parent gadget
//redifine last acquisition gadget
iframe_top_gadget = true;
tmp_constructor.declareService(function () {
return listenHashChange(this);
});
setAqParent(root_gadget, last_acquisition_gadget);
} else {
throw error;
......
......@@ -16,9 +16,13 @@
RenderJSIframeGadget = __RenderJSIframeGadget;
// Keep track of the root gadget
renderJS(window).ready(function (g) {
root_gadget_defer.resolve(g);
});
renderJS(window)
.ready(function (g) {
root_gadget_defer.resolve(g);
})
.declareMethod("render", function (options) {
this.__last_options = options;
});
QUnit.config.testTimeout = 1000;
......@@ -453,7 +457,11 @@
var instance;
equal(Klass.prototype.__path, url);
deepEqual(Klass.prototype.__acquired_method_dict, {});
deepEqual(
Klass.prototype.__acquired_method_dict,
{pleasePublishMyState:
Klass.prototype.__acquired_method_dict.pleasePublishMyState}
);
equal(Klass.prototype.__foo, 'bar');
equal(Klass.__template_element.nodeType, 9);
......@@ -522,7 +530,11 @@
var instance;
equal(Klass.prototype.__path, url);
deepEqual(Klass.prototype.__acquired_method_dict, {});
deepEqual(
Klass.prototype.__acquired_method_dict,
{pleasePublishMyState:
Klass.prototype.__acquired_method_dict.pleasePublishMyState}
);
instance = new Klass();
ok(instance instanceof RenderJSGadget);
......@@ -2269,7 +2281,11 @@
gadget.declareGadget(url)//, document.getElementById('qunit-fixture'))
.then(function (new_gadget) {
equal(new_gadget.__path, url);
deepEqual(new_gadget.__acquired_method_dict, {});
deepEqual(
new_gadget.__acquired_method_dict,
{pleasePublishMyState:
new_gadget.__acquired_method_dict.pleasePublishMyState}
);
ok(new_gadget instanceof RenderJSGadget);
})
.always(function () {
......@@ -3591,8 +3607,15 @@
// Check instance
equal(root_gadget.__path,
root_gadget_path_without_hash);
equal(typeof root_gadget.__acquired_method_dict, 'object');
equal(Object.keys(root_gadget.__acquired_method_dict).length, 1);
deepEqual(
root_gadget.__acquired_method_dict,
{
pleasePublishMyState:
root_gadget.__acquired_method_dict.pleasePublishMyState,
getTopURL:
root_gadget.__acquired_method_dict.getTopURL
}
);
equal(root_gadget.__title, document.title);
deepEqual(root_gadget.__interface_list, []);
deepEqual(root_gadget.__required_css_list,
......@@ -3642,7 +3665,7 @@
ok(root_gadget.__aq_parent !== undefined);
ok(root_gadget.hasOwnProperty("__sub_gadget_dict"));
deepEqual(root_gadget.__sub_gadget_dict, {});
deepEqual(root_gadget_klass.__service_list, []);
deepEqual(root_gadget_klass.__service_list.length, 1);
return new RSVP.Queue()
.push(function () {
return root_gadget.getTopURL().then(function (topURL) {
......@@ -3720,5 +3743,126 @@
});
});
/////////////////////////////////////////////////////////////////
// RenderJSGadget URL
/////////////////////////////////////////////////////////////////
module("RenderJSGadget URL", {
setup: function () {
renderJS.clearGadgetKlassList();
this.server = sinon.fakeServer.create();
this.server.autoRespond = true;
this.server.autoRespondAfter = 5;
},
teardown: function () {
this.server.restore();
delete this.server;
}
});
test('Check that the root gadget renders on hashchange', function () {
var root_gadget,
rand_int = Math.random();
stop();
root_gadget_defer.promise
.then(function (gadget) {
root_gadget = gadget;
window.location.hash = 'a=b&c=de&f.g=h' + rand_int;
return RSVP.delay(200);
})
.then(function () {
deepEqual(
root_gadget.__last_options,
{a: 'b', c: 'de', f: {g: 'h' + rand_int}}
);
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('pleasePublishMyState on root gadget', function () {
stop();
root_gadget_defer.promise
.then(function (root_gadget) {
return root_gadget.aq_pleasePublishMyState({a: 'b', c: {d: 'e'}});
})
.then(function (result) {
equal(result, '#a=b&c.d=e');
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
});
test('pleasePublishMyState rejected on anonymous subgadget', function () {
// Check that declare gadget returns the gadget
var url = 'https://example.org/files/qunittest/test',
html = "<html><body></body></html>";
this.server.respondWith("GET", url, [200, {
"Content-Type": "text/html"
}, html]);
stop();
renderJS.declareGadgetKlass(url)
.then(function (root_gadget) {
return root_gadget_defer.promise;
})
.then(function (root_gadget) {
return root_gadget.declareGadget(url);
})
.then(function (new_gadget) {
return new_gadget.aq_pleasePublishMyState({a: 'b', c: {d: 'e'}});
})
.then(function () {
ok(false, "aq_pleasePublishMyState should fail in anonymous");
})
.fail(function (error) {
ok(error instanceof Error);
equal(error.message, "gadget scope is mandatory");
})
.always(function () {
start();
});
});
test('pleasePublishMyState includes gadget scope', function () {
// Check that declare gadget returns the gadget
var url = 'https://example.org/files/qunittest/test',
html = "<html><body></body></html>";
this.server.respondWith("GET", url, [200, {
"Content-Type": "text/html"
}, html]);
stop();
renderJS.declareGadgetKlass(url)
.then(function (root_gadget) {
return root_gadget_defer.promise;
})
.then(function (root_gadget) {
return root_gadget.declareGadget(url, {scope: "foo"});
})
.then(function (new_gadget) {
return new_gadget.aq_pleasePublishMyState({a: 'b', c: {d: 'e'}});
})
.then(function (result) {
equal(result, "#foo.a=b&foo.c.d=e");
})
.fail(function (error) {
ok(false, error);
})
.always(function () {
start();
});
});
}(document, renderJS, QUnit, sinon, URI));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment