Commit 9a2ca855 authored by Romain Courteaud's avatar Romain Courteaud

Use promise a+ library to correctly propagate error.

Replace jQuery by RSVP.
Allow to declareGadget without attaching it to the DOM.
Correctly handle error.

IFrame are currently broken.
parent 9b2a662b
...@@ -16,12 +16,14 @@ all: external lint test build doc ...@@ -16,12 +16,14 @@ all: external lint test build doc
external: lib/sinon/sinon.js \ external: lib/sinon/sinon.js \
lib/sinon/sinon-qunit.js \ lib/sinon/sinon-qunit.js \
lib/jquery/jquery.js \ lib/jquery/jquery.js \
lib/rsvp/rsvp.js \
lib/jschannel/jschannel.js \ lib/jschannel/jschannel.js \
lib/require/require.js \ lib/require/require.js \
lib/qunit/qunit.js \ lib/qunit/qunit.js \
lib/qunit/qunit.css \ lib/qunit/qunit.css \
lib/jio/jio.js \ lib/jio/jio.js \
lib/jio/md5.js \ lib/jio/md5.js \
lib/jio/sha256.js \
lib/jio/complex_queries.js \ lib/jio/complex_queries.js \
lib/jio/localstorage.js lib/jio/localstorage.js
...@@ -31,13 +33,17 @@ lib/sinon/sinon.js: ...@@ -31,13 +33,17 @@ lib/sinon/sinon.js:
lib/sinon/sinon-qunit.js: lib/sinon/sinon-qunit.js:
@mkdir -p $(@D) @mkdir -p $(@D)
# curl -s -o $@ http://sinonjs.org/releases/sinon-qunit-1.0.0.js curl -s -o $@ http://sinonjs.org/releases/sinon-qunit-1.0.0.js
curl -s -o $@ https://raw.github.com/jfromaniello/jmail/master/scripts/Tests/sinon-qunit-1.0.0.js # curl -s -o $@ https://raw.github.com/jfromaniello/jmail/master/scripts/Tests/sinon-qunit-1.0.0.js
lib/jquery/jquery.js: lib/jquery/jquery.js:
@mkdir -p $(@D) @mkdir -p $(@D)
curl -s -o $@ http://code.jquery.com/jquery-2.0.3.js curl -s -o $@ http://code.jquery.com/jquery-2.0.3.js
lib/rsvp/rsvp.js:
@mkdir -p $(@D)
curl -s -o $@ http://git.erp5.org/gitweb/rsvp.js.git/blob_plain/HEAD:/dist/rsvp-2.0.4.js
lib/jschannel/jschannel.js: lib/jschannel/jschannel.js:
@mkdir -p $(@D) @mkdir -p $(@D)
curl -s -o $@ http://mozilla.github.io/jschannel/src/jschannel.js curl -s -o $@ http://mozilla.github.io/jschannel/src/jschannel.js
...@@ -56,15 +62,19 @@ lib/jio/jio.js: ...@@ -56,15 +62,19 @@ lib/jio/jio.js:
lib/jio/md5.js: lib/jio/md5.js:
@mkdir -p $(@D) @mkdir -p $(@D)
curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/refs/heads/master:/lib/md5/md5.js curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/HEAD:/src/md5.amd.js
lib/jio/sha256.js:
@mkdir -p $(@D)
curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/HEAD:/src/sha256.amd.js
lib/jio/localstorage.js: lib/jio/localstorage.js:
@mkdir -p $(@D) @mkdir -p $(@D)
curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/refs/heads/master:/src/jio.storage/localstorage.js curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/HEAD:/src/jio.storage/localstorage.js
lib/jio/complex_queries.js: lib/jio/complex_queries.js:
@mkdir -p $(@D) @mkdir -p $(@D)
curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/refs/heads/master:/complex_queries.js curl -s -o $@ http://git.erp5.org/gitweb/jio.git/blob_plain/HEAD:/complex_queries.js
$(RENDERJS_MIN): $(RENDERJS) $(RENDERJS_MIN): $(RENDERJS)
$(UGLIFY_CMD) "$<" > "$@" $(UGLIFY_CMD) "$<" > "$@"
...@@ -88,4 +98,4 @@ lint: ${BUILDDIR}/$(RENDERJS).lint ...@@ -88,4 +98,4 @@ lint: ${BUILDDIR}/$(RENDERJS).lint
doc: doc:
$(YUIDOC_CMD) . $(YUIDOC_CMD) .
clean: clean:
rm -rf $(RENDERJS_MIN) ${BUILDDIR} lib/sinon lib/jquery lib/jschannel lib/qunit lib/jio lib/require rm -rf $(RENDERJS_MIN) ${BUILDDIR} lib/sinon lib/jquery lib/jschannel lib/qunit lib/jio lib/require lib/rsvp
# npm install uglify-js # npm install uglify-js
UGLIFY_CMD = $(shell which uglifyjs || echo node ~/node_modules/uglify-js/bin/uglifyjs) UGLIFY_CMD = ~/node_modules/.bin/uglifyjs
# npm install jslint # npm install jslint
LINT_CMD = $(shell which jslint || echo node ~/node_modules/jslint/bin/jslint.js) --terse LINT_CMD = /home/romain/devel/nowm/gidzit/node_modules/.bin/jslint
YUIDOC_CMD = $(shell which yuidoc) YUIDOC_CMD = $(shell which yuidoc)
PHANTOMJS_CMD = xvfb-run phantomjs PHANTOMJS_CMD = xvfb-run phantomjs
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<head> <head>
<title>Ace Editor</title> <title>Ace Editor</title>
<script src="./ace/ace.js" type="text/javascript" charset="utf-8"></script> <script src="./ace/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="aceeditor.js" type="text/javascript"></script> <script src="aceeditor.js" type="text/javascript"></script>
......
/*global window, jQuery, rJS, ace */ /*global window, rJS, ace */
"use strict"; "use strict";
(function (window, $, rJS) { (function (window, rJS) {
var gk = rJS(window); var gk = rJS(window);
gk.declareMethod('setContent', function (value) { gk.declareMethod('setContent', function (value) {
rJS(this).editor.getSession().setValue(value); rJS(this).editor.getSession().setValue(value);
// return rJS(this).context.find('textarea').val(escape_text(value));
}) })
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return rJS(this).editor.getSession().getValue(); return rJS(this).editor.getSession().getValue();
// return rJS(this).context.find('textarea').val();
}); });
gk.ready(function () { gk.ready(function (g) {
var g = rJS(this); g.editor = ace.edit(g.element.getElementsByTagName('div')[0]);
g.editor = ace.edit("editor");
g.editor.setTheme("ace/theme/monokai"); g.editor.setTheme("ace/theme/monokai");
// g.context.find("textarea").jqte();
// editor.setTheme("ace/theme/twilight");
// editor.getSession().setMode("ace/mode/javascript");
}); });
}(window, jQuery, rJS, ace)); }(window, rJS, ace));
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<head> <head>
<title>Catalog Gadget</title> <title>Catalog Gadget</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="catalog.js" type="text/javascript"></script> <script src="catalog.js" type="text/javascript"></script>
......
/*global window, jQuery, rJS */ /*global window, jQuery, rJS */
"use strict"; "use strict";
(function (window, $, rJS, undefined) { (function (window, $, rJS) {
var gk = rJS(window), var gk = rJS(window),
io_dict = { io_dict = {
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<head> <head>
<title>Simple Text Editor Gadget</title> <title>Simple Text Editor Gadget</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="editor.js" type="text/javascript"></script> <script src="editor.js" type="text/javascript"></script>
......
/*global window, jQuery, rJS */ /*global window, rJS */
"use strict"; "use strict";
(function (window, $, rJS) { (function (window, rJS) {
function escape_text(text) { function escape_text(text) {
// &, ", ', <, >, / // &, ", ', <, >, /
...@@ -12,10 +12,11 @@ ...@@ -12,10 +12,11 @@
var gk = rJS(window); var gk = rJS(window);
gk.declareMethod('setContent', function (value) { gk.declareMethod('setContent', function (value) {
return rJS(this).context.find('textarea').val(escape_text(value)); rJS(this).element.getElementsByTagName('textarea')[0].value =
escape_text(value);
}) })
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return rJS(this).context.find('textarea').val(); return rJS(this).element.getElementsByTagName('textarea')[0].value;
}); });
}(window, jQuery, rJS)); }(window, rJS));
...@@ -2,10 +2,10 @@ ...@@ -2,10 +2,10 @@
<head> <head>
<title>IO</title> <title>IO</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="../../lib/jio/md5.js" type="text/javascript"></script> <script src="../../lib/jio/sha256.js" type="text/javascript"></script>
<script src="../../lib/jio/jio.js" type="text/javascript"></script> <script src="../../lib/jio/jio.js" type="text/javascript"></script>
<script src="../../lib/jio/complex_queries.js" type="text/javascript"></script> <script src="../../lib/jio/complex_queries.js" type="text/javascript"></script>
<script src="../../lib/jio/localstorage.js" type="text/javascript"></script> <script src="../../lib/jio/localstorage.js" type="text/javascript"></script>
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
var gk = rJS(window); var gk = rJS(window);
gk.declareMethod('configureIO', function (key) { gk.declareMethod('configureIO', function (key) {
rJS(this).jio = jIO.newJio({ rJS(this).jio = jIO.createJIO({
"type": "local", "type": "local",
"username": "couscous", "username": "couscous",
"application_name": "renderjs" "application_name": "renderjs"
...@@ -14,60 +14,36 @@ ...@@ -14,60 +14,36 @@
}) })
.declareMethod('getIO', function () { .declareMethod('getIO', function () {
var deferred = $.Deferred(), var gadget = rJS(this);
default_value = "",
gadget = rJS(this);
gadget.jio.getAttachment({ return gadget.jio.getAttachment({
"_id": gadget.jio_key, "_id": gadget.jio_key,
"_attachment": "body.txt" "_attachment": "body.txt"
}, function (err, response) { }).then(function (response) {
if (err) { return jIO.util.readBlobAsText(response.data);
if (err.status === 404) { }).then(function (response) {
deferred.resolve(default_value); return response.target.result;
} else {
deferred.reject(err);
}
} else {
deferred.resolve(response || default_value);
}
}); });
return deferred.promise();
}) })
.declareMethod('setIO', function (value) { .declareMethod('setIO', function (value) {
var gadget = rJS(this);
var deferred = $.Deferred(),
default_value = "", return gadget.jio.put({"_id": gadget.jio_key})
gadget = rJS(this); .then(function () {
return gadget.jio.putAttachment({
gadget.jio.put({"_id": gadget.jio_key}, "_id": gadget.jio_key,
function (err, response) { "_attachment": "body.txt",
if (err) { "_data": value,
deferred.reject(err); "_mimetype": "text/plain"
} else { });
gadget.jio.putAttachment({
"_id": gadget.jio_key,
"_attachment": "body.txt",
"_data": value,
"_mimetype": "text/plain"
}, function (err, response) {
if (err) {
deferred.reject(err);
} else {
deferred.resolve();
}
});
}
}); });
return deferred.promise();
}) })
.declareMethod('configureDataSourceCallback', function (that, callback) { .declareMethod('configureDataSourceCallback', function (that, callback) {
var g = rJS(this); var g = rJS(this);
g.context.find('a').unbind('click').click(function () { $(g.element).find('a').unbind('click').click(function () {
callback.apply(that).done(function (value) { callback.apply(that).then(function (value) {
g.setIO(value); g.setIO(value);
}); });
}); });
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<title>JQuery Text Editor Gadget</title> <title>JQuery Text Editor Gadget</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="jqte/jquery-te-1.4.0.css" /> <link rel="stylesheet" href="jqte/jquery-te-1.4.0.css" />
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="jqte/jquery-te-1.4.0.js" type="text/javascript"></script> <script src="jqte/jquery-te-1.4.0.js" type="text/javascript"></script>
......
/*global window, jQuery, rJS */ /*global window, rJS, jQuery */
"use strict"; "use strict";
(function (window, $, rJS) { (function (window, rJS, $) {
var gk = rJS(window); var gk = rJS(window);
gk.declareMethod('setContent', function (value) { gk.declareMethod('setContent', function (value) {
// return rJS(this).context.find('textarea').val(escape_text(value)); // return rJS(this).context.find('textarea').val(escape_text(value));
return rJS(this).context.find('#textarea-b').jqteVal(value); return $(rJS(this).element).find('#textarea-b').jqteVal(value);
}) })
.declareMethod('getContent', function () { .declareMethod('getContent', function () {
return rJS(this).context.find('#textarea-b').val(); return $(rJS(this).element).find('#textarea-b').val();
}); });
gk.ready(function () { gk.ready(function (g) {
var g = rJS(this); $(g.element).find("#textarea-b").jqte();
g.context.find("#textarea-b").jqte();
}); });
}(window, jQuery, rJS)); }(window, rJS, jQuery));
...@@ -3,5 +3,5 @@ iframe { ...@@ -3,5 +3,5 @@ iframe {
margin:0; margin:0;
padding:0; padding:0;
width:80%; width:80%;
height:100px; height:300px;
} }
...@@ -6,9 +6,10 @@ ...@@ -6,9 +6,10 @@
<title>Office JS</title> <title>Office JS</title>
<link rel="stylesheet" href="../../lib/jqm/jquery.mobile.css" /> <link rel="stylesheet" href="../../lib/jqm/jquery.mobile.css" />
<link rel="stylesheet" href="officejs.css" /> <link rel="stylesheet" href="officejs.css" />
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../../renderjs.js" type="text/javascript"></script> <script src="../../renderjs.js" type="text/javascript"></script>
<script src="../../lib/jquery/jquery.js" type="text/javascript"></script>
<script src="officejs.js" type="text/javascript"></script> <script src="officejs.js" type="text/javascript"></script>
<script src="../../lib/jqm/jquery.mobile.js" type="text/javascript"></script> <script src="../../lib/jqm/jquery.mobile.js" type="text/javascript"></script>
<link rel="http://www.renderjs.org/rel/interface" <link rel="http://www.renderjs.org/rel/interface"
...@@ -32,7 +33,7 @@ ...@@ -32,7 +33,7 @@
<!-- panel --> <!-- panel -->
<div data-role="panel" id="menu" data-theme="a" class="bare_panel"></div> <div data-role="panel" id="menu" data-theme="a" class="bare_panel"></div>
<div class="catalog_location"></div> <!--div class="catalog_location"></div-->
<!-- content --> <!-- content -->
<div data-role="content"> <div data-role="content">
......
/*global window, jQuery, rJS */ /*global document, window, jQuery, rJS, RSVP */
"use strict"; "use strict";
(function (window, $, rJS) { (function (window, $, rJS, RSVP) {
function attachIOToEditor(editor, io, id) { function attachIOToEditor(all_param) {
editor.context.trigger('create'); var editor = all_param[0],
io.context.trigger('create'); io = all_param[1],
id = all_param[2];
$(io.element).trigger('create');
$(editor.element).trigger('create');
// .then(function (element) {
// element.trigger('create');
// });
// io.getElement()
// .then(function (element) {
// element.trigger('create');
// });
io.configureIO(id).done(function () { return io.configureIO(id)
io.configureDataSourceCallback(editor, editor.getContent); .then(function () {
io.getIO().done(function (data) { return io.configureDataSourceCallback(editor, editor.getContent);
editor.setContent(data); })
.then(function () {
return io.getIO().fail(function (error) {
if (error.status === 404) {
return "";
}
throw error;
});
})
.then(function (value) {
return editor.setContent(value);
}); });
});
} }
rJS(window).ready(function () { function handleError(rejectedReason) {
var g = rJS(this), var word_list;
catalog_context = g.context.find(".catalog_location").last(), console.warn(rejectedReason);
editor_a_context = g.context.find(".editor_a").last(), if (rejectedReason instanceof Error) {
io_a_context = g.context.find(".editor_a_safe").last(); word_list = rejectedReason.toString();
} else {
word_list = JSON.stringify(rejectedReason);
}
// XXX Escape text
document.getElementsByTagName('body')[0].innerHTML = word_list;
throw rejectedReason;
}
function createLoadNewEditorCallback(g, editor_path, e_c, io_path, i_c) {
// throw new Error("nutnut");
console.log("createLoadNewEditorCallback");
return function () {
// var new_element = document.createElement("div");
// // console.log(e_c);
// // e_c[0].innerHTML = '';
e_c.empty();
// e_c[0].appendChild(new_element);
console.log("inside");
return RSVP.all([
g.declareGadget(editor_path, {element: e_c[0]}),
// g.declareGadget(editor_path),
g.declareGadget(io_path),
"officejs"
])
.then(function (all_param) {
// e_c.empty();
// e_c[0].appendChild(all_param[0].element);
i_c.empty();
i_c[0].appendChild(all_param[1].element);
return attachIOToEditor(all_param);
})
.fail(handleError);
};
}
rJS(window).ready(function (g) {
var editor_a_context = $(g.element).find(".editor_a").last(),
io_a_context = $(g.element).find(".editor_a_safe").last();
// editor_b_context = g.context.find(".editor_b").last(), // editor_b_context = g.context.find(".editor_b").last(),
// io_b_context = g.context.find(".editor_b_safe").last(); // io_b_context = g.context.find(".editor_b_safe").last();
// First, load the catalog gadget // First, load the catalog gadget
g.declareGadget('./catalog.html', catalog_context).done( g.declareGadget('./catalog.html')
function (catalog) { .then(function (catalog) {
// Fetch the list of editor and io gadgets // Fetch the list of editor and io gadgets
// This is done in 2 different queries to the catalog // This is done in 2 different queries to the catalog
$.when( return RSVP.all([
catalog.allDocs( catalog.allDocs(
{query: 'interface: "http://www.renderjs.org/interface/editor"'} {query: 'interface: "http://www.renderjs.org/interface/editor"'}
), ),
catalog.allDocs( catalog.allDocs(
{query: 'interface: "http://www.renderjs.org/interface/io"'} {query: 'interface: "http://www.renderjs.org/interface/io"'}
) )
).done(function (editor_list, io_list) { ]);
var panel_context = g.context.find(".bare_panel"); })
.then(function (all_list) {
var panel_context = $(g.element).find(".bare_panel"),
editor_list = all_list[0],
io_list = all_list[1],
editor_definition,
i;
// Load 1 editor and 1 IO and plug them // Load 1 editor and 1 IO and plug them
$.when( console.log(io_list[0].path);
g.declareIframedGadget(editor_list[0].path, editor_a_context), return RSVP.all([
g.declareGadget(io_list[0].path, io_a_context), g.declareGadget(editor_list[0].path),// editor_a_context),
"officejs" g.declareGadget(io_list[0].path),// io_a_context),
).done(attachIOToEditor); "officejs"
])
.then(function (all_param) {
editor_a_context.empty();
console.log("first G");
console.log(all_param[0].element);
editor_a_context[0].appendChild(all_param[0].element);
console.log(editor_a_context[0]);
io_a_context.empty();
io_a_context[0].appendChild(all_param[1].element);
return attachIOToEditor(all_param);
})
.then(function () {
// Fill the panel
for (i = 0; i < editor_list.length; i += 1) {
editor_definition = editor_list[i];
panel_context.append(
'<a href="#" data-role="button" data-icon="edit" ' +
'data-iconpos="left">' + editor_definition.title + '</a>'
);
// $(editor_definition.element).click(
panel_context.find('a').last().click(
createLoadNewEditorCallback(g, editor_definition.path,
editor_a_context, io_list[0].path, io_a_context)
);
// XXX Handle links
// panel_context.find('a').last().click(function () {
// $.when(
// g.declareGadget(editor_definition.path,
// editor_a_context),
// g.declareGadget(io_list[0].path, io_a_context),
// "officejs"
// ).done(attachIOToEditor);
// });
}
panel_context.trigger('create');
});
// Fill the panel })
$.each(editor_list, function (i, editor_definition) {
panel_context.append(
'<a href="#" data-role="button" data-icon="edit" ' +
'data-iconpos="left">' + editor_definition.title + '</a>'
);
panel_context.find('a').last().click(function () {
$.when(
g.declareIframedGadget(editor_definition.path,
editor_a_context),
g.declareGadget(io_list[0].path, io_a_context),
"officejs"
).done(attachIOToEditor);
});
});
panel_context.trigger('create');
});
}
);
.fail(handleError);
// $.when( // $.when(
// g.declareGadget('./jqteditor.html', editor_a_context), // g.declareGadget('./jqteditor.html', editor_a_context),
// g.declareGadget('./io.html', io_a_context), // g.declareGadget('./io.html', io_a_context),
...@@ -76,4 +158,4 @@ ...@@ -76,4 +158,4 @@
// "officejs_b").done(attachIOToEditor); // "officejs_b").done(attachIOToEditor);
}); });
}(window, jQuery, rJS)); }(window, jQuery, rJS, RSVP));
...@@ -9,10 +9,20 @@ ...@@ -9,10 +9,20 @@
* *
* @module complex_queries * @module complex_queries
*/ */
var complex_queries; // define([module_name], [dependencies], module);
(function () { (function (dependencies, module) {
"use strict"; "use strict";
var to_export = {}, module_name = "complex_queries"; if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(exports);
}
window.complex_queries = {};
module(window.complex_queries);
}(['exports'], function (to_export) {
"use strict";
/** /**
* Add a secured (write permission denied) property to an object. * Add a secured (write permission denied) property to an object.
* *
...@@ -28,6 +38,7 @@ var complex_queries; ...@@ -28,6 +38,7 @@ var complex_queries;
"value": value "value": value
}); });
} }
/** /**
* Parse a text request to a json query object tree * Parse a text request to a json query object tree
* *
...@@ -36,6 +47,7 @@ var complex_queries; ...@@ -36,6 +47,7 @@ var complex_queries;
*/ */
function parseStringToObject(string) { function parseStringToObject(string) {
/* /*
Default template driver for JS/CC generated parsers running as Default template driver for JS/CC generated parsers running as
browser-based JavaScript/ECMAScript applications. browser-based JavaScript/ECMAScript applications.
...@@ -718,343 +730,157 @@ if ((error_count = __NODEJS_parse(string, error_offsets, error_lookaheads)) > 0) ...@@ -718,343 +730,157 @@ if ((error_count = __NODEJS_parse(string, error_offsets, error_lookaheads)) > 0)
} }
} }
return result; return result;
} // parseStringToObject } // parseStringToObject
_export('parseStringToObject', parseStringToObject); _export('parseStringToObject', parseStringToObject);
/*jslint indent: 2, maxlen: 80, sloppy: true */
var query_class_dict = {};
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export: true */ /*global Query: true, query_class_dict: true, inherits: true,
_export: true, QueryFactory: true */
/** /**
* Escapes regexp special chars from a string. * The ComplexQuery inherits from Query, and compares one or several metadata
* values.
* *
* @param {String} string The string to escape * @class ComplexQuery
* @return {String} The escaped string * @extends Query
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="AND"] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
*/ */
function stringEscapeRegexpCharacters(string) { function ComplexQuery(spec) {
if (typeof string === "string") { Query.call(this);
return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1");
} /**
throw new TypeError("complex_queries.stringEscapeRegexpCharacters(): " + * Logical operator to use to compare object values
"Argument no 1 is not of type 'string'"); *
* @attribute operator
* @type String
* @default "AND"
* @optional
*/
this.operator = spec.operator || "AND";
/**
* The sub Query list which are used to query an item.
*
* @attribute query_list
* @type Array
* @default []
* @optional
*/
this.query_list = spec.query_list || [];
this.query_list = this.query_list.map(QueryFactory.create);
} }
inherits(ComplexQuery, Query);
_export("stringEscapeRegexpCharacters", stringEscapeRegexpCharacters); /**
* #crossLink "Query/match:method"
*/
ComplexQuery.prototype.match = function (item, wildcard_character) {
return this[this.operator](item, wildcard_character);
};
/** /**
* Convert metadata values to array of strings. ex: * #crossLink "Query/toString:method"
*
* "a" -> ["a"],
* {"content": "a"} -> ["a"]
*
* @param {Any} value The metadata value
* @return {Array} The value in string array format
*/ */
function metadataValueToStringArray(value) { ComplexQuery.prototype.toString = function () {
var i, new_value = []; var str_list = ["("], this_operator = this.operator;
if (value === undefined) { this.query_list.forEach(function (query) {
return undefined; str_list.push(query.toString());
} str_list.push(this_operator);
if (!Array.isArray(value)) { });
value = [value]; str_list.pop(); // remove last operator
} str_list.push(")");
for (i = 0; i < value.length; i += 1) { return str_list.join(" ");
if (typeof value[i] === 'object') { };
new_value[i] = value[i].content;
} else {
new_value[i] = value[i];
}
}
return new_value;
}
/** /**
* A sort function to sort items by key * #crossLink "Query/serialized:method"
*
* @param {String} key The key to sort on
* @param {String} [way="ascending"] 'ascending' or 'descending'
* @return {Function} The sort function
*/ */
function sortFunction(key, way) { ComplexQuery.prototype.serialized = function () {
if (way === 'descending') { var s = {
return function (a, b) { "type": "complex",
// this comparison is 5 times faster than json comparison "operator": this.operator,
var i, l; "query_list": []
a = metadataValueToStringArray(a[key]) || []; };
b = metadataValueToStringArray(b[key]) || []; this.query_list.forEach(function (query) {
l = a.length > b.length ? a.length : b.length; s.query_list.push(query.serialized());
for (i = 0; i < l; i += 1) { });
if (a[i] === undefined) { return s;
return 1; };
}
if (b[i] === undefined) {
return -1;
}
if (a[i] > b[i]) {
return -1;
}
if (a[i] < b[i]) {
return 1;
}
}
return 0;
};
}
if (way === 'ascending') {
return function (a, b) {
// this comparison is 5 times faster than json comparison
var i, l;
a = metadataValueToStringArray(a[key]) || [];
b = metadataValueToStringArray(b[key]) || [];
l = a.length > b.length ? a.length : b.length;
for (i = 0; i < l; i += 1) {
if (a[i] === undefined) {
return -1;
}
if (b[i] === undefined) {
return 1;
}
if (a[i] > b[i]) {
return 1;
}
if (a[i] < b[i]) {
return -1;
}
}
return 0;
};
}
throw new TypeError("complex_queries.sortFunction(): " +
"Argument 2 must be 'ascending' or 'descending'");
}
/** /**
* Clones all native object in deep. Managed types: Object, Array, String, * Comparison operator, test if all sub queries match the
* Number, Boolean, null. * item value
* *
* @param {A} object The object to clone * @method AND
* @return {A} The cloned object * @param {Object} item The item to match
* @param {String} wildcard_character The wildcard character
* @return {Boolean} true if all match, false otherwise
*/ */
function deepClone(object) { ComplexQuery.prototype.AND = function (item, wildcard_character) {
var i, cloned; var i;
if (Array.isArray(object)) { for (i = 0; i < this.query_list.length; i += 1) {
cloned = []; if (!this.query_list[i].match(item, wildcard_character)) {
for (i = 0; i < object.length; i += 1) { return false;
cloned[i] = deepClone(object[i]);
}
return cloned;
}
if (typeof object === "object") {
cloned = {};
for (i in object) {
if (object.hasOwnProperty(i)) {
cloned[i] = deepClone(object[i]);
}
} }
return cloned;
} }
return object; return true;
} };
/** /**
* Inherits the prototype methods from one constructor into another. The * Comparison operator, test if one of the sub queries matches the
* prototype of `constructor` will be set to a new object created from * item value
* `superConstructor`.
* *
* @param {Function} constructor The constructor which inherits the super one * @method OR
* @param {Function} superConstructor The super constructor * @param {Object} item The item to match
* @param {String} wildcard_character The wildcard character
* @return {Boolean} true if one match, false otherwise
*/ */
function inherits(constructor, superConstructor) { ComplexQuery.prototype.OR = function (item, wildcard_character) {
constructor.super_ = superConstructor; var i;
constructor.prototype = Object.create(superConstructor.prototype, { for (i = 0; i < this.query_list.length; i += 1) {
"constructor": { if (this.query_list[i].match(item, wildcard_character)) {
"configurable": true, return true;
"enumerable": false,
"writable": true,
"value": constructor
} }
}); }
} return false;
};
/** /**
* Does nothing * Comparison operator, test if the sub query does not match the
* item value
*
* @method NOT
* @param {Object} item The item to match
* @param {String} wildcard_character The wildcard character
* @return {Boolean} true if one match, false otherwise
*/ */
function emptyFunction() {} ComplexQuery.prototype.NOT = function (item, wildcard_character) {
return !this.query_list[0].match(item, wildcard_character);
};
query_class_dict.complex = ComplexQuery;
_export("ComplexQuery", ComplexQuery);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global parseStringToObject: true, emptyFunction: true, sortOn: true, limit:
true, select: true, _export: true, stringEscapeRegexpCharacters: true,
deepClone: true */
/** /**
* Filter a list of items, modifying them to select only wanted keys. If * The query to use to filter a list of objects.
* `clone` is true, then the method will act on a cloned list. * This is an abstract class.
*
* @param {Array} select_option Key list to keep
* @param {Array} list The item list to filter
* @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Array} The filtered list
*/
function select(select_option, list, clone) {
var i, j, new_item;
if (!Array.isArray(select_option)) {
throw new TypeError("complex_queries.select(): " +
"Argument 1 is not of type Array");
}
if (!Array.isArray(list)) {
throw new TypeError("complex_queries.select(): " +
"Argument 2 is not of type Array");
}
if (clone === true) {
list = deepClone(list);
}
for (i = 0; i < list.length; i += 1) {
new_item = {};
for (j = 0; j < select_option.length; j += 1) {
new_item[select_option[j]] = list[i][select_option[j]];
}
for (j in new_item) {
if (new_item.hasOwnProperty(j)) {
list[i] = new_item;
break;
}
}
}
return list;
}
_export('select', select);
/**
* Sort a list of items, according to keys and directions. If `clone` is true,
* then the method will act on a cloned list.
*
* @param {Array} sort_on_option List of couples [key, direction]
* @param {Array} list The item list to sort
* @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Array} The filtered list
*/
function sortOn(sort_on_option, list, clone) {
var sort_index;
if (!Array.isArray(sort_on_option)) {
throw new TypeError("complex_queries.sortOn(): " +
"Argument 1 is not of type 'array'");
}
if (clone) {
list = deepClone(list);
}
for (sort_index = sort_on_option.length - 1; sort_index >= 0;
sort_index -= 1) {
list.sort(sortFunction(
sort_on_option[sort_index][0],
sort_on_option[sort_index][1]
));
}
return list;
}
_export('sortOn', sortOn);
/**
* Limit a list of items, according to index and length. If `clone` is true,
* then the method will act on a cloned list.
*
* @param {Array} limit_option A couple [from, length]
* @param {Array} list The item list to limit
* @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Array} The filtered list
*/
function limit(limit_option, list, clone) {
if (!Array.isArray(limit_option)) {
throw new TypeError("complex_queries.limit(): " +
"Argument 1 is not of type 'array'");
}
if (!Array.isArray(list)) {
throw new TypeError("complex_queries.limit(): " +
"Argument 2 is not of type 'array'");
}
if (clone) {
list = deepClone(list);
}
list.splice(0, limit_option[0]);
if (limit_option[1]) {
list.splice(limit_option[1]);
}
return list;
}
_export('limit', limit);
/**
* Convert a search text to a regexp.
*
* @param {String} string The string to convert
* @param {String} [wildcard_character=undefined] The wildcard chararter
* @return {RegExp} The search text regexp
*/
function convertStringToRegExp(string, wildcard_character) {
if (typeof string !== 'string') {
throw new TypeError("complex_queries.convertStringToRegExp(): " +
"Argument 1 is not of type 'string'");
}
if (wildcard_character === undefined ||
wildcard_character === null || wildcard_character === '') {
return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
}
if (typeof wildcard_character !== 'string' || wildcard_character.length > 1) {
throw new TypeError("complex_queries.convertStringToRegExp(): " +
"Optional argument 2 must be a string of length <= 1");
}
return new RegExp("^" + stringEscapeRegexpCharacters(string).replace(
new RegExp(stringEscapeRegexpCharacters(wildcard_character), 'g'),
'.*'
) + "$");
}
_export('convertStringToRegExp', convertStringToRegExp);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export: true, ComplexQuery: true, SimpleQuery: true, Query: true,
parseStringToObject: true */
var query_class_dict = {};
/**
* Provides static methods to create Query object
*
* @class QueryFactory
*/
function QueryFactory() {}
/**
* Creates Query object from a search text string or a serialized version
* of a Query.
*
* @method create
* @static
* @param {Object,String} object The search text or the serialized version
* of a Query
* @return {Query} A Query object
*/
QueryFactory.create = function (object) {
if (object === "") {
return new Query();
}
if (typeof object === "string") {
object = parseStringToObject(object);
}
if (typeof (object || {}).type === "string" &&
query_class_dict[object.type]) {
return new query_class_dict[object.type](object);
}
throw new TypeError("QueryFactory.create(): " +
"Argument 1 is not a search text or a parsable object");
};
_export("QueryFactory", QueryFactory);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global parseStringToObject: true, emptyFunction: true, sortOn: true, limit:
true, select: true, _export: true, stringEscapeRegexpCharacters: true,
deepClone: true */
/**
* The query to use to filter a list of objects.
* This is an abstract class.
* *
* @class Query * @class Query
* @constructor * @constructor
...@@ -1128,7 +954,7 @@ Query.prototype.exec = function (item_list, option) { ...@@ -1128,7 +954,7 @@ Query.prototype.exec = function (item_list, option) {
option.wildcard_character = '%'; option.wildcard_character = '%';
} }
while (i < item_list.length) { while (i < item_list.length) {
if (!this.match(item_list[i], option.wildcard_character)) { if (!item_list[i] || !this.match(item_list[i], option.wildcard_character)) {
item_list.splice(i, 1); item_list.splice(i, 1);
} else { } else {
i += 1; i += 1;
...@@ -1148,9 +974,10 @@ Query.prototype.exec = function (item_list, option) { ...@@ -1148,9 +974,10 @@ Query.prototype.exec = function (item_list, option) {
* *
* @method match * @method match
* @param {Object} item The object to test * @param {Object} item The object to test
* @param {String} wildcard_character The wildcard character to use
* @return {Boolean} true if match, false otherwise * @return {Boolean} true if match, false otherwise
*/ */
Query.prototype.match = function (item, wildcard_character) { Query.prototype.match = function () {
return true; return true;
}; };
...@@ -1219,6 +1046,70 @@ Query.prototype.serialized = function () { ...@@ -1219,6 +1046,70 @@ Query.prototype.serialized = function () {
}; };
_export("Query", Query); _export("Query", Query);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export, ComplexQuery, SimpleQuery, Query, parseStringToObject,
query_class_dict */
/**
* Provides static methods to create Query object
*
* @class QueryFactory
*/
function QueryFactory() {
return;
}
/**
* Creates Query object from a search text string or a serialized version
* of a Query.
*
* @method create
* @static
* @param {Object,String} object The search text or the serialized version
* of a Query
* @return {Query} A Query object
*/
QueryFactory.create = function (object) {
if (object === "") {
return new Query();
}
if (typeof object === "string") {
object = parseStringToObject(object);
}
if (typeof (object || {}).type === "string" &&
query_class_dict[object.type]) {
return new query_class_dict[object.type](object);
}
throw new TypeError("QueryFactory.create(): " +
"Argument 1 is not a search text or a parsable object");
};
_export("QueryFactory", QueryFactory);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global _export: true, to_export: true */
function objectToSearchText(query) {
var str_list = [];
if (query.type === "complex") {
str_list.push("(");
(query.query_list || []).forEach(function (sub_query) {
str_list.push(objectToSearchText(sub_query));
str_list.push(query.operator);
});
str_list.length -= 1;
str_list.push(")");
return str_list.join(" ");
}
if (query.type === "simple") {
return query.id + (query.id ? ": " : "") + (query.operator || "=") + ' "' +
query.value + '"';
}
throw new TypeError("This object is not a query");
}
_export("objectToSearchText", objectToSearchText);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global Query: true, inherits: true, query_class_dict: true, _export: true, /*global Query: true, inherits: true, query_class_dict: true, _export: true,
convertStringToRegExp: true */ convertStringToRegExp: true */
...@@ -1460,156 +1351,297 @@ SimpleQuery.prototype[">="] = function (object_value, comparison_value) { ...@@ -1460,156 +1351,297 @@ SimpleQuery.prototype[">="] = function (object_value, comparison_value) {
query_class_dict.simple = SimpleQuery; query_class_dict.simple = SimpleQuery;
_export("SimpleQuery", SimpleQuery); _export("SimpleQuery", SimpleQuery);
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */
/*global Query: true, query_class_dict: true, inherits: true, /*global _export: true */
_export: true, QueryFactory: true */
/** /**
* The ComplexQuery inherits from Query, and compares one or several metadata * Escapes regexp special chars from a string.
* values.
* *
* @class ComplexQuery * @param {String} string The string to escape
* @extends Query * @return {String} The escaped string
* @param {Object} [spec={}] The specifications
* @param {String} [spec.operator="AND"] The compare method to use
* @param {String} spec.key The metadata key
* @param {String} spec.value The value of the metadata to compare
*/ */
function ComplexQuery(spec) { function stringEscapeRegexpCharacters(string) {
Query.call(this); if (typeof string === "string") {
return string.replace(/([\\\.\$\[\]\(\)\{\}\^\?\*\+\-])/g, "\\$1");
/** }
* Logical operator to use to compare object values throw new TypeError("complex_queries.stringEscapeRegexpCharacters(): " +
* "Argument no 1 is not of type 'string'");
* @attribute operator }
* @type String
* @default "AND"
* @optional
*/
this.operator = spec.operator || "AND";
/** _export("stringEscapeRegexpCharacters", stringEscapeRegexpCharacters);
* The sub Query list which are used to query an item.
*
* @attribute query_list
* @type Array
* @default []
* @optional
*/
this.query_list = spec.query_list || [];
this.query_list = this.query_list.map(QueryFactory.create);
/**
* Convert metadata values to array of strings. ex:
*
* "a" -> ["a"],
* {"content": "a"} -> ["a"]
*
* @param {Any} value The metadata value
* @return {Array} The value in string array format
*/
function metadataValueToStringArray(value) {
var i, new_value = [];
if (value === undefined) {
return undefined;
}
if (!Array.isArray(value)) {
value = [value];
}
for (i = 0; i < value.length; i += 1) {
if (typeof value[i] === 'object') {
new_value[i] = value[i].content;
} else {
new_value[i] = value[i];
}
}
return new_value;
} }
inherits(ComplexQuery, Query);
/** /**
* #crossLink "Query/match:method" * A sort function to sort items by key
*
* @param {String} key The key to sort on
* @param {String} [way="ascending"] 'ascending' or 'descending'
* @return {Function} The sort function
*/ */
ComplexQuery.prototype.match = function (item, wildcard_character) { function sortFunction(key, way) {
return this[this.operator](item, wildcard_character); if (way === 'descending') {
}; return function (a, b) {
// this comparison is 5 times faster than json comparison
var i, l;
a = metadataValueToStringArray(a[key]) || [];
b = metadataValueToStringArray(b[key]) || [];
l = a.length > b.length ? a.length : b.length;
for (i = 0; i < l; i += 1) {
if (a[i] === undefined) {
return 1;
}
if (b[i] === undefined) {
return -1;
}
if (a[i] > b[i]) {
return -1;
}
if (a[i] < b[i]) {
return 1;
}
}
return 0;
};
}
if (way === 'ascending') {
return function (a, b) {
// this comparison is 5 times faster than json comparison
var i, l;
a = metadataValueToStringArray(a[key]) || [];
b = metadataValueToStringArray(b[key]) || [];
l = a.length > b.length ? a.length : b.length;
for (i = 0; i < l; i += 1) {
if (a[i] === undefined) {
return -1;
}
if (b[i] === undefined) {
return 1;
}
if (a[i] > b[i]) {
return 1;
}
if (a[i] < b[i]) {
return -1;
}
}
return 0;
};
}
throw new TypeError("complex_queries.sortFunction(): " +
"Argument 2 must be 'ascending' or 'descending'");
}
/** /**
* #crossLink "Query/toString:method" * Clones all native object in deep. Managed types: Object, Array, String,
* Number, Boolean, null.
*
* @param {A} object The object to clone
* @return {A} The cloned object
*/ */
ComplexQuery.prototype.toString = function () { function deepClone(object) {
var str_list = ["("], this_operator = this.operator; var i, cloned;
this.query_list.forEach(function (query) { if (Array.isArray(object)) {
str_list.push(query.toString()); cloned = [];
str_list.push(this_operator); for (i = 0; i < object.length; i += 1) {
}); cloned[i] = deepClone(object[i]);
str_list.pop(); // remove last operator }
str_list.push(")"); return cloned;
return str_list.join(" "); }
}; if (typeof object === "object") {
cloned = {};
for (i in object) {
if (object.hasOwnProperty(i)) {
cloned[i] = deepClone(object[i]);
}
}
return cloned;
}
return object;
}
/** /**
* #crossLink "Query/serialized:method" * Inherits the prototype methods from one constructor into another. The
* prototype of `constructor` will be set to a new object created from
* `superConstructor`.
*
* @param {Function} constructor The constructor which inherits the super one
* @param {Function} superConstructor The super constructor
*/ */
ComplexQuery.prototype.serialized = function () { function inherits(constructor, superConstructor) {
var s = { constructor.super_ = superConstructor;
"type": "complex", constructor.prototype = Object.create(superConstructor.prototype, {
"operator": this.operator, "constructor": {
"query_list": [] "configurable": true,
}; "enumerable": false,
this.query_list.forEach(function (query) { "writable": true,
s.query_list.push(query.serialized()); "value": constructor
}
}); });
return s; }
};
/** /**
* Comparison operator, test if all sub queries match the * Does nothing
* item value */
function emptyFunction() {
return;
}
/**
* Filter a list of items, modifying them to select only wanted keys. If
* `clone` is true, then the method will act on a cloned list.
* *
* @method AND * @param {Array} select_option Key list to keep
* @param {Object} item The item to match * @param {Array} list The item list to filter
* @param {String} wildcard_character The wildcard character * @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Boolean} true if all match, false otherwise * @return {Array} The filtered list
*/ */
ComplexQuery.prototype.AND = function (item, wildcard_character) { function select(select_option, list, clone) {
var i; var i, j, new_item;
for (i = 0; i < this.query_list.length; i += 1) { if (!Array.isArray(select_option)) {
if (!this.query_list[i].match(item, wildcard_character)) { throw new TypeError("complex_queries.select(): " +
return false; "Argument 1 is not of type Array");
}
if (!Array.isArray(list)) {
throw new TypeError("complex_queries.select(): " +
"Argument 2 is not of type Array");
}
if (clone === true) {
list = deepClone(list);
}
for (i = 0; i < list.length; i += 1) {
new_item = {};
for (j = 0; j < select_option.length; j += 1) {
if (list[i].hasOwnProperty([select_option[j]])) {
new_item[select_option[j]] = list[i][select_option[j]];
}
}
for (j in new_item) {
if (new_item.hasOwnProperty(j)) {
list[i] = new_item;
break;
}
} }
} }
return true; return list;
}; }
_export('select', select);
/** /**
* Comparison operator, test if one of the sub queries matches the * Sort a list of items, according to keys and directions. If `clone` is true,
* item value * then the method will act on a cloned list.
* *
* @method OR * @param {Array} sort_on_option List of couples [key, direction]
* @param {Object} item The item to match * @param {Array} list The item list to sort
* @param {String} wildcard_character The wildcard character * @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Boolean} true if one match, false otherwise * @return {Array} The filtered list
*/ */
ComplexQuery.prototype.OR = function (item, wildcard_character) { function sortOn(sort_on_option, list, clone) {
var i; var sort_index;
for (i = 0; i < this.query_list.length; i += 1) { if (!Array.isArray(sort_on_option)) {
if (this.query_list[i].match(item, wildcard_character)) { throw new TypeError("complex_queries.sortOn(): " +
return true; "Argument 1 is not of type 'array'");
}
} }
return false; if (clone) {
}; list = deepClone(list);
}
for (sort_index = sort_on_option.length - 1; sort_index >= 0;
sort_index -= 1) {
list.sort(sortFunction(
sort_on_option[sort_index][0],
sort_on_option[sort_index][1]
));
}
return list;
}
_export('sortOn', sortOn);
/** /**
* Comparison operator, test if the sub query does not match the * Limit a list of items, according to index and length. If `clone` is true,
* item value * then the method will act on a cloned list.
* *
* @method NOT * @param {Array} limit_option A couple [from, length]
* @param {Object} item The item to match * @param {Array} list The item list to limit
* @param {String} wildcard_character The wildcard character * @param {Boolean} [clone=false] If true, modifies a clone of the list
* @return {Boolean} true if one match, false otherwise * @return {Array} The filtered list
*/ */
ComplexQuery.prototype.NOT = function (item, wildcard_character) { function limit(limit_option, list, clone) {
return !this.query_list[0].match(item, wildcard_character); if (!Array.isArray(limit_option)) {
}; throw new TypeError("complex_queries.limit(): " +
"Argument 1 is not of type 'array'");
query_class_dict.complex = ComplexQuery; }
if (!Array.isArray(list)) {
throw new TypeError("complex_queries.limit(): " +
"Argument 2 is not of type 'array'");
}
if (clone) {
list = deepClone(list);
}
list.splice(0, limit_option[0]);
if (limit_option[1]) {
list.splice(limit_option[1]);
}
return list;
}
_export("ComplexQuery", ComplexQuery); _export('limit', limit);
if (typeof define === "function" && define.amd) { /**
define(to_export); * Convert a search text to a regexp.
} else if (typeof window === "object") { *
Object.defineProperty(window, module_name, { * @param {String} string The string to convert
configurable: false, * @param {String} [wildcard_character=undefined] The wildcard chararter
enumerable: true, * @return {RegExp} The search text regexp
writable: false, */
value: to_export function convertStringToRegExp(string, wildcard_character) {
}); if (typeof string !== 'string') {
} else if (typeof exports === "object") { throw new TypeError("complex_queries.convertStringToRegExp(): " +
var i; "Argument 1 is not of type 'string'");
for (i in to_export) { }
if (to_export.hasOwnProperty(i)) { if (wildcard_character === undefined ||
exports[i] = to_export[i]; wildcard_character === null || wildcard_character === '') {
} return new RegExp("^" + stringEscapeRegexpCharacters(string) + "$");
} }
} else { if (typeof wildcard_character !== 'string' || wildcard_character.length > 1) {
complex_queries = to_export; throw new TypeError("complex_queries.convertStringToRegExp(): " +
"Optional argument 2 must be a string of length <= 1");
} }
}()); return new RegExp("^" + stringEscapeRegexpCharacters(string).replace(
new RegExp(stringEscapeRegexpCharacters(wildcard_character), 'g'),
'.*'
) + "$");
}
_export('convertStringToRegExp', convertStringToRegExp);
return to_export;
}));
This source diff could not be displayed because it is too large. You can view the blob instead.
/* /*
* Copyright 2013, Nexedi SA * Copyright 2013, Nexedi SA
* Released under the LGPL license. * Released under the LGPL license.
* http://www.gnu.org/licenses/lgpl.html * http://www.gnu.org/licenses/lgpl.html
*/ */
/*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true */ /*jslint indent: 2, maxlen: 80, sloppy: true, nomen: true, regexp: true */
/*global jIO: true, localStorage: true, setTimeout: true, /*global jIO, localStorage, setTimeout, complex_queries, window, define,
complex_queries: true */ exports, require */
/** /**
* JIO Local Storage. Type = 'local'. * JIO Local Storage. Type = 'local'.
...@@ -16,6 +16,9 @@ ...@@ -16,6 +16,9 @@
* *
* { * {
* "type": "local", * "type": "local",
* "mode": <string>,
* // - "localStorage" // default
* // - "memory"
* "username": <non empty string>, // to define user space * "username": <non empty string>, // to define user space
* "application_name": <string> // default 'untitled' * "application_name": <string> // default 'untitled'
* } * }
...@@ -43,12 +46,42 @@ ...@@ -43,12 +46,42 @@
* *
* @class LocalStorage * @class LocalStorage
*/ */
jIO.addStorageType('local', function (spec, my) {
spec = spec || {}; // define([module_name], [dependencies], module);
var that, priv, localstorage; (function (dependencies, module) {
that = my.basicStorage(spec, my); "use strict";
priv = {}; if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(exports, require('jio'), require('complex_queries'));
}
window.local_storage = {};
module(window.local_storage, jIO, complex_queries);
}([
'exports',
'jio',
'complex_queries'
], function (exports, jIO, complex_queries) {
"use strict";
/**
* Checks if an object has no enumerable keys
*
* @param {Object} obj The object
* @return {Boolean} true if empty, else false
*/
function objectIsEmpty(obj) {
var k;
for (k in obj) {
if (obj.hasOwnProperty(k)) {
return false;
}
}
return true;
}
var ram = {}, memorystorage, localstorage;
/* /*
* Wrapper for the localStorage used to simplify instion of any kind of * Wrapper for the localStorage used to simplify instion of any kind of
...@@ -67,371 +100,637 @@ jIO.addStorageType('local', function (spec, my) { ...@@ -67,371 +100,637 @@ jIO.addStorageType('local', function (spec, my) {
} }
}; };
// attributes /*
priv.username = spec.username || ''; * Wrapper for the localStorage used to simplify instion of any kind of
priv.application_name = spec.application_name || 'untitled'; * values
priv.localpath = 'jio/localstorage/' + priv.username + '/' +
priv.application_name;
// ==================== Tools ====================
/**
* Generate a new uuid
* @method generateUuid
* @return {string} The new uuid
*/ */
priv.generateUuid = function () { memorystorage = {
var S4 = function () { getItem: function (item) {
/* 65536 */ var value = ram[item];
var i, string = Math.floor( return value === undefined ? null : JSON.parse(value);
Math.random() * 0x10000 },
).toString(16); setItem: function (item, value) {
for (i = string.length; i < 4; i += 1) { ram[item] = JSON.stringify(value);
string = '0' + string; },
} removeItem: function (item) {
return string; delete ram[item];
}; }
return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() +
S4() + S4();
}; };
/** /**
* Checks if an object has no enumerable keys * The JIO LocalStorage extension
* @method objectIsEmpty *
* @param {object} obj The object * @class LocalStorage
* @return {boolean} true if empty, else false * @constructor
*/ */
priv.objectIsEmpty = function (obj) { function LocalStorage(spec) {
var k; if (typeof spec.username !== 'string' && !spec.username) {
for (k in obj) { throw new TypeError("LocalStorage 'username' must be a string " +
if (obj.hasOwnProperty(k)) { "which contains more than one character.");
return false;
}
} }
return true; this._localpath = 'jio/localstorage/' + spec.username + '/' + (
}; spec.application_name === null || spec.application_name ===
undefined ? 'untitled' : spec.application_name.toString()
// ===================== overrides ====================== );
that.specToStore = function () { switch (spec.mode) {
return { case "memory":
"application_name": priv.application_name, this._database = ram;
"username": priv.username this._storage = memorystorage;
}; this._mode = "memory";
}; break;
default:
that.validateState = function () { this._database = localStorage;
if (typeof priv.username === "string" && priv.username !== '') { this._storage = localstorage;
return ''; this._mode = "localStorage";
break;
} }
return 'Need at least one parameter: "username".'; }
};
// ==================== commands ====================
/** /**
* Create a document in local storage. * Create a document in local storage.
*
* @method post * @method post
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/ */
that.post = function (command) { LocalStorage.prototype.post = function (command, metadata) {
setTimeout(function () { var doc, doc_id = metadata._id;
var doc, doc_id = command.getDocId(); if (!doc_id) {
if (!doc_id) { doc_id = jIO.util.generateUuid();
doc_id = priv.generateUuid(); }
} doc = this._storage.getItem(this._localpath + "/" + doc_id);
doc = localstorage.getItem(priv.localpath + "/" + doc_id); if (doc === null) {
if (doc === null) { // the document does not exist
// the document does not exist doc = jIO.util.deepClone(metadata);
doc = command.cloneDoc(); doc._id = doc_id;
doc._id = doc_id; delete doc._attachments;
delete doc._attachments; this._storage.setItem(this._localpath + "/" + doc_id, doc);
localstorage.setItem(priv.localpath + "/" + doc_id, doc); command.success({"id": doc_id});
that.success({ } else {
"ok": true, // the document already exists
"id": doc_id command.error(
}); "conflict",
} else { "document exists",
// the document already exists "Cannot create a new document"
that.error({ );
"status": 409, }
"statusText": "Conflicts",
"error": "conflicts",
"message": "Cannot create a new document",
"reason": "Document already exists"
});
}
});
}; };
/** /**
* Create or update a document in local storage. * Create or update a document in local storage.
*
* @method put * @method put
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} metadata The metadata to store
* @param {Object} options The command options
*/ */
that.put = function (command) { LocalStorage.prototype.put = function (command, metadata) {
setTimeout(function () { var doc, tmp, status;
var doc, tmp; doc = this._storage.getItem(this._localpath + "/" + metadata._id);
doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); if (doc === null) {
if (doc === null) { // the document does not exist
// the document does not exist doc = jIO.util.deepClone(metadata);
doc = command.cloneDoc(); delete doc._attachments;
delete doc._attachments; status = "created";
} else { } else {
// the document already exists // the document already exists
tmp = command.cloneDoc(); tmp = jIO.util.deepClone(metadata);
tmp._attachments = doc._attachments; tmp._attachments = doc._attachments;
doc = tmp; doc = tmp;
} status = "no_content";
// write }
localstorage.setItem(priv.localpath + "/" + command.getDocId(), doc); // write
that.success({ this._storage.setItem(this._localpath + "/" + metadata._id, doc);
"ok": true, command.success(status);
"id": command.getDocId()
});
});
}; };
/** /**
* Add an attachment to a document * Add an attachment to a document
* @method putAttachment *
* @param {object} command The JIO command * @method putAttachment
* @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.putAttachment = function (command) { LocalStorage.prototype.putAttachment = function (command, param) {
setTimeout(function () { var that = this, doc, status = "created";
var doc; doc = this._storage.getItem(this._localpath + "/" + param._id);
doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); if (doc === null) {
if (doc === null) { // the document does not exist
// the document does not exist return command.error(
that.error({ "not_found",
"status": 404, "missing",
"statusText": "Not Found", "Impossible to add attachment"
"error": "not_found", );
"message": "Impossible to add attachment", }
"reason": "Document not found"
});
return;
}
// the document already exists // the document already exists
// download data
jIO.util.readBlobAsBinaryString(param._blob).then(function (e) {
doc._attachments = doc._attachments || {}; doc._attachments = doc._attachments || {};
doc._attachments[command.getAttachmentId()] = { if (doc._attachments[param._attachment]) {
"content_type": command.getAttachmentMimeType(), status = "no_content";
"digest": "md5-" + command.md5SumAttachmentData(), }
"length": command.getAttachmentLength() doc._attachments[param._attachment] = {
"content_type": param._blob.type,
"digest": jIO.util.makeBinaryStringDigest(e.target.result),
"length": param._blob.size
}; };
// upload data that._storage.setItem(that._localpath + "/" + param._id + "/" +
localstorage.setItem(priv.localpath + "/" + command.getDocId() + "/" + param._attachment, e.target.result);
command.getAttachmentId(), that._storage.setItem(that._localpath + "/" + param._id, doc);
command.getAttachmentData()); command.success(status,
// write document {"digest": doc._attachments[param._attachment].digest});
localstorage.setItem(priv.localpath + "/" + command.getDocId(), doc); }, function (e) {
that.success({ command.error(
"ok": true, "request_timeout",
"id": command.getDocId(), "blob error",
"attachment": command.getAttachmentId() "Error " + e.status + ", unable to get blob content"
}); );
}, function (e) {
command.notify((e.loaded / e.total) * 100);
}); });
}; };
/** /**
* Get a document * Get a document
*
* @method get * @method get
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.get = function (command) { LocalStorage.prototype.get = function (command, param) {
setTimeout(function () { var doc = this._storage.getItem(
var doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); this._localpath + "/" + param._id
if (doc !== null) { );
that.success(doc); if (doc !== null) {
} else { command.success({"data": doc});
that.error({ } else {
"status": 404, command.error(
"statusText": "Not Found", "not_found",
"error": "not_found", "missing",
"message": "Cannot find the document", "Cannot find document"
"reason": "Document does not exist" );
}); }
}
});
}; };
/** /**
* Get a attachment * Get an attachment
*
* @method getAttachment * @method getAttachment
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.getAttachment = function (command) { LocalStorage.prototype.getAttachment = function (command, param) {
setTimeout(function () { var doc;
var doc = localstorage.getItem(priv.localpath + "/" + command.getDocId() + doc = this._storage.getItem(this._localpath + "/" + param._id);
"/" + command.getAttachmentId()); if (doc === null) {
if (doc !== null) { return command.error(
that.success(doc); "not_found",
} else { "missing document",
that.error({ "Cannot find document"
"status": 404, );
"statusText": "Not Found", }
"error": "not_found",
"message": "Cannot find the attachment", if (typeof doc._attachments !== 'object' ||
"reason": "Attachment does not exist" typeof doc._attachments[param._attachment] !== 'object') {
}); return command.error(
} "not_found",
"missing attachment",
"Cannot find attachment"
);
}
command.success({
"data": this._storage.getItem(
this._localpath + "/" + param._id +
"/" + param._attachment
) || "",
"digest": doc._attachments[param._attachment].digest,
"content_type": doc._attachments[param._attachment].content_type || ""
}); });
}; };
/** /**
* Remove a document * Remove a document
*
* @method remove * @method remove
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.remove = function (command) { LocalStorage.prototype.remove = function (command, param) {
setTimeout(function () { var doc, i, attachment_list;
var doc, i, attachment_list; doc = this._storage.getItem(this._localpath + "/" + param._id);
doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); attachment_list = [];
attachment_list = []; if (doc !== null && typeof doc === "object") {
if (doc !== null && typeof doc === "object") { if (typeof doc._attachments === "object") {
if (typeof doc._attachments === "object") { // prepare list of attachments
// prepare list of attachments for (i in doc._attachments) {
for (i in doc._attachments) { if (doc._attachments.hasOwnProperty(i)) {
if (doc._attachments.hasOwnProperty(i)) { attachment_list.push(i);
attachment_list.push(i);
}
} }
} }
} else {
return that.error({
"status": 404,
"statusText": "Not Found",
"error": "not_found",
"message": "Document not found",
"reason": "missing"
});
}
localstorage.removeItem(priv.localpath + "/" + command.getDocId());
// delete all attachments
for (i = 0; i < attachment_list.length; i += 1) {
localstorage.removeItem(priv.localpath + "/" + command.getDocId() +
"/" + attachment_list[i]);
} }
that.success({ } else {
"ok": true, return command.error(
"id": command.getDocId() "not_found",
}); "missing",
}); "Document not found"
);
}
this._storage.removeItem(this._localpath + "/" + param._id);
// delete all attachments
for (i = 0; i < attachment_list.length; i += 1) {
this._storage.removeItem(this._localpath + "/" + param._id +
"/" + attachment_list[i]);
}
command.success();
}; };
/** /**
* Remove an attachment * Remove an attachment
*
* @method removeAttachment * @method removeAttachment
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.removeAttachment = function (command) { LocalStorage.prototype.removeAttachment = function (command, param) {
setTimeout(function () { var doc = this._storage.getItem(this._localpath + "/" + param._id);
var doc, error, i, attachment_list; if (typeof doc !== 'object') {
error = function (word) { return command.error(
that.error({ "not_found",
"status": 404, "missing document",
"statusText": "Not Found", "Document not found"
"error": "not_found", );
"message": word + " not found", }
"reason": "missing" if (typeof doc._attachments !== "object" ||
}); typeof doc._attachments[param._attachment] !== "object") {
}; return command.error(
doc = localstorage.getItem(priv.localpath + "/" + command.getDocId()); "not_found",
// remove attachment from document "missing attachment",
if (doc !== null && typeof doc === "object" && "Attachment not found"
typeof doc._attachments === "object") { );
if (typeof doc._attachments[command.getAttachmentId()] === }
"object") {
delete doc._attachments[command.getAttachmentId()]; delete doc._attachments[param._attachment];
if (priv.objectIsEmpty(doc._attachments)) { if (objectIsEmpty(doc._attachments)) {
delete doc._attachments; delete doc._attachments;
} }
localstorage.setItem(priv.localpath + "/" + command.getDocId(), this._storage.setItem(this._localpath + "/" + param._id, doc);
doc); this._storage.removeItem(this._localpath + "/" + param._id +
localstorage.removeItem(priv.localpath + "/" + command.getDocId() + "/" + param._attachment);
"/" + command.getAttachmentId()); command.success();
that.success({
"ok": true,
"id": command.getDocId(),
"attachment": command.getAttachmentId()
});
} else {
error("Attachment");
}
} else {
error("Document");
}
});
}; };
/** /**
* Get all filenames belonging to a user from the document index * Get all filenames belonging to a user from the document index
*
* @method allDocs * @method allDocs
* @param {object} command The JIO command * @param {Object} command The JIO command
* @param {Object} param The given parameters
* @param {Object} options The command options
*/ */
that.allDocs = function (command) { LocalStorage.prototype.allDocs = function (command, param, options) {
var i, row, path_re, rows = [], document_list = [], option, document_object; var i, row, path_re, rows, document_list, document_object, delete_id;
param.unused = true;
rows = [];
document_list = [];
path_re = new RegExp( path_re = new RegExp(
"^" + complex_queries.stringEscapeRegexpCharacters(priv.localpath) + "^" + complex_queries.stringEscapeRegexpCharacters(this._localpath) +
"/[^/]+$" "/[^/]+$"
); );
option = command.cloneOption(); if (options.query === undefined && options.sort_on === undefined &&
if (typeof complex_queries !== "object" || options.select_list === undefined &&
(option.query === undefined && option.sort_on === undefined && options.include_docs === undefined) {
option.select_list === undefined &&
option.include_docs === undefined)) {
rows = []; rows = [];
for (i in localStorage) { for (i in this._database) {
if (localStorage.hasOwnProperty(i)) { if (this._database.hasOwnProperty(i)) {
// filter non-documents // filter non-documents
if (path_re.test(i)) { if (path_re.test(i)) {
row = { value: {} }; row = { value: {} };
row.id = i.split('/').slice(-1)[0]; row.id = i.split('/').slice(-1)[0];
row.key = row.id; row.key = row.id;
if (command.getOption('include_docs')) { if (options.include_docs) {
row.doc = JSON.parse(localStorage.getItem(i)); row.doc = JSON.parse(this._storage.getItem(i));
} }
rows.push(row); rows.push(row);
} }
} }
} }
that.success({"rows": rows, "total_rows": rows.length}); command.success({"data": {"rows": rows, "total_rows": rows.length}});
} else { } else {
// create complex query object from returned results // create complex query object from returned results
for (i in localStorage) { for (i in this._database) {
if (localStorage.hasOwnProperty(i)) { if (this._database.hasOwnProperty(i)) {
if (path_re.test(i)) { if (path_re.test(i)) {
document_list.push(localstorage.getItem(i)); document_list.push(this._storage.getItem(i));
} }
} }
} }
option.select_list = option.select_list || []; options.select_list = options.select_list || [];
option.select_list.push("_id"); if (options.select_list.indexOf("_id") === -1) {
if (option.include_docs === true) { options.select_list.push("_id");
delete_id = true;
}
if (options.include_docs === true) {
document_object = {}; document_object = {};
document_list.forEach(function (meta) { document_list.forEach(function (meta) {
document_object[meta._id] = meta; document_object[meta._id] = meta;
}); });
} }
complex_queries.QueryFactory.create(option.query || ""). complex_queries.QueryFactory.create(options.query || "").
exec(document_list, option); exec(document_list, options);
document_list = document_list.map(function (value) { document_list = document_list.map(function (value) {
var o = { var o = {
"id": value._id, "id": value._id,
"key": value._id "key": value._id
}; };
if (option.include_docs === true) { if (options.include_docs === true) {
o.doc = document_object[value._id]; o.doc = document_object[value._id];
delete document_object[value._id]; delete document_object[value._id];
} }
delete value._id; if (delete_id) {
delete value._id;
}
o.value = value; o.value = value;
return o; return o;
}); });
that.success({"total_rows": document_list.length, command.success({"data": {
"rows": document_list}); "total_rows": document_list.length,
"rows": document_list
}});
}
};
/**
* Check the storage or a specific document
*
* @method check
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.check = function (command, param) {
this.genericRepair(command, param, false);
};
/**
* Repair the storage or a specific document
*
* @method repair
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Object} options The command options
*/
LocalStorage.prototype.repair = function (command, param) {
this.genericRepair(command, param, true);
};
/**
* A generic method that manage check or repair command
*
* @method genericRepair
* @param {Object} command The JIO command
* @param {Object} param The command parameters
* @param {Boolean} repair If true then repair else just check
*/
LocalStorage.prototype.genericRepair = function (command, param, repair) {
var that = this, result;
function referenceAttachment(param, attachment) {
if (param.referenced_attachments.indexOf(attachment) !== -1) {
return;
}
var i = param.unreferenced_attachments.indexOf(attachment);
if (i !== -1) {
param.unreferenced_attachments.splice(i, 1);
}
param.referenced_attachments[param.referenced_attachments.length] =
attachment;
}
function attachmentFound(param, attachment) {
if (param.referenced_attachments.indexOf(attachment) !== -1) {
return;
}
if (param.unreferenced_attachments.indexOf(attachment) !== -1) {
return;
}
param.unreferenced_attachments[param.unreferenced_attachments.length] =
attachment;
}
function repairOne(param, repair) {
var i, doc, modified;
doc = that._storage.getItem(that._localpath + "/" + param._id);
if (doc === null) {
return; // OK
}
// check document type
if (typeof doc !== 'object') {
// wrong document
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Document is unrecoverable"
]};
}
// delete the document
that._storage.removeItem(that._localpath + "/" + param._id);
return; // OK
}
// good document type
// repair json document
if (!repair) {
if (!(new jIO.Metadata(doc).check())) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Some metadata might be lost"
]};
}
} else {
modified = jIO.util.uniqueJSONStringify(doc) !==
jIO.util.uniqueJSONStringify(new jIO.Metadata(doc).format()._dict);
}
if (doc._attachments !== undefined) {
if (typeof doc._attachments !== 'object') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Attachments are unrecoverable"
]};
}
delete doc._attachments;
that._storage.setItem(that._localpath + "/" + param._id, doc);
return; // OK
}
for (i in doc._attachments) {
if (doc._attachments.hasOwnProperty(i)) {
// check attachment existence
if (that._storage.getItem(that._localpath + "/" + param._id + "/" +
i) !== 'string') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"missing attachment",
"Attachment \"" + i + "\" of \"" + param._id + "\" is missing"
]};
}
delete doc._attachments[i];
if (objectIsEmpty(doc._attachments)) {
delete doc._attachments;
}
modified = true;
} else {
// attachment exists
// check attachment metadata
// check length
referenceAttachment(param, param._id + "/" + doc._attachments[i]);
if (doc._attachments[i].length !== undefined &&
typeof doc._attachments[i].length !== 'number') {
if (!repair) {
return {"error": true, "answers": [
"conflict",
"corrupted",
"Attachment metadata length corrupted"
]};
}
// It could take a long time to get the length, no repair.
// length can be omited
delete doc._attachments[i].length;
}
// It could take a long time to regenerate the hash, no check.
// Impossible to discover the attachment content type.
}
}
}
}
if (modified) {
that._storage.setItem(that._localpath + "/" + param._id, doc);
}
// OK
}
function repairAll(param, repair) {
var i, result;
for (i in that._database) {
if (that._database.hasOwnProperty(i)) {
// browsing every entry
if (i.slice(0, that._localpath.length) === that._localpath) {
// is part of the user space
if (/^[^\/]+\/[^\/]+$/.test(i.slice(that._localpath.length + 1))) {
// this is an attachment
attachmentFound(param, i.slice(that._localpath.length + 1));
} else if (/^[^\/]+$/.test(i.slice(that._localpath.length + 1))) {
// this is a document
param._id = i.slice(that._localpath.length + 1);
result = repairOne(param, repair);
if (result) {
return result;
}
} else {
// this is pollution
that._storage.removeItem(i);
}
}
}
}
// remove unreferenced attachments
for (i = 0; i < param.unreferenced_attachments.length; i += 1) {
that._storage.removeItem(that._localpath + "/" +
param.unreferenced_attachments[i]);
}
}
param.referenced_attachments = [];
param.unreferenced_attachments = [];
if (typeof param._id === 'string') {
result = repairOne(param, repair) || {};
} else {
result = repairAll(param, repair) || {};
}
if (result.error) {
return command.error.apply(command, result.answers || []);
} }
command.success.apply(command, result.answers || []);
}; };
return that; jIO.addStorage('local', LocalStorage);
});
//////////////////////////////////////////////////////////////////////
// Tools
function createLocalDescription(username, application_name) {
if (typeof username !== 'string') {
throw new TypeError("LocalStorage username must be a string");
}
var description = {
"type": "local",
"username": username
};
if (typeof application_name === 'string') {
description.application_name = application_name;
}
return description;
}
function createMemoryDescription(username, application_name) {
var description = createLocalDescription(username, application_name);
description.mode = "memory";
return description;
}
/**
* Tool to help users to create local storage description for JIO
*
* @param {String} username The username
* @param {String} [application_name] The application_name
* @param {String} [mode="localStorage"] Use localStorage or memory
* @return {Object} The storage description
*/
function createDescription(username, application_name, mode) {
if (mode === undefined || mode.toString() === 'localStorage') {
return createLocalDescription(username, application_name);
}
if (mode.toString() === 'memory') {
return createMemoryDescription(username, application_name);
}
throw new TypeError("Unknown LocalStorage '" + mode.toString() + "' mode");
}
exports.createDescription = createDescription;
exports.createLocalDescription = createLocalDescription;
exports.createMemoryDescription = createMemoryDescription;
function clearLocalStorage() {
var k;
for (k in localStorage) {
if (localStorage.hasOwnProperty(k)) {
if (/^jio\/localstorage\//.test(k)) {
localStorage.removeItem(k);
}
}
}
}
function clearMemoryStorage() {
jIO.util.dictClear(ram);
}
exports.clear = clearLocalStorage;
exports.clearLocalStorage = clearLocalStorage;
exports.clearMemoryStorage = clearMemoryStorage;
}));
(function (dependencies, module) {
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(exports);
}
module(window);
}(['exports'], function (exports) {
/* /*
* A JavaScript implementation of the RSA Data Security, Inc. MD5 Message * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
* Digest Algorithm, as defined in RFC 1321. * Digest Algorithm, as defined in RFC 1321.
...@@ -377,3 +386,11 @@ function bit_rol(num, cnt) ...@@ -377,3 +386,11 @@ function bit_rol(num, cnt)
{ {
return (num << cnt) | (num >>> (32 - cnt)); return (num << cnt) | (num >>> (32 - cnt));
} }
exports.hex_md5 = hex_md5;
exports.b64_md5 = b64_md5;
exports.any_md5 = any_md5;
exports.hex_hmac_md5 = hex_hmac_md5;
exports.b64_hmac_md5 = b64_hmac_md5;
exports.any_hmac_md5 = any_hmac_md5;
}));
(function (dependencies, module) {
if (typeof define === 'function' && define.amd) {
return define(dependencies, module);
}
if (typeof exports === 'object') {
return module(exports);
}
module(window);
}(['exports'], function (window) {
/* A JavaScript implementation of the Secure Hash Algorithm, SHA-256
* Version 0.3 Copyright Angel Marin 2003-2004 - http://anmar.eu.org/
* Distributed under the BSD License
* Some bits taken from Paul Johnston's SHA-1 implementation
*/
(function () {
var chrsz = 8; /* bits per input character. 8 - ASCII; 16 - Unicode */
function safe_add (x, y) {
var lsw = (x & 0xFFFF) + (y & 0xFFFF);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xFFFF);
}
function S (X, n) {return ( X >>> n ) | (X << (32 - n));}
function R (X, n) {return ( X >>> n );}
function Ch(x, y, z) {return ((x & y) ^ ((~x) & z));}
function Maj(x, y, z) {return ((x & y) ^ (x & z) ^ (y & z));}
function Sigma0256(x) {return (S(x, 2) ^ S(x, 13) ^ S(x, 22));}
function Sigma1256(x) {return (S(x, 6) ^ S(x, 11) ^ S(x, 25));}
function Gamma0256(x) {return (S(x, 7) ^ S(x, 18) ^ R(x, 3));}
function Gamma1256(x) {return (S(x, 17) ^ S(x, 19) ^ R(x, 10));}
function newArray (n) {
var a = [];
for (;n>0;n--) {
a.push(undefined);
}
return a;
}
function core_sha256 (m, l) {
var K = [0x428A2F98,0x71374491,0xB5C0FBCF,0xE9B5DBA5,0x3956C25B,0x59F111F1,0x923F82A4,0xAB1C5ED5,0xD807AA98,0x12835B01,0x243185BE,0x550C7DC3,0x72BE5D74,0x80DEB1FE,0x9BDC06A7,0xC19BF174,0xE49B69C1,0xEFBE4786,0xFC19DC6,0x240CA1CC,0x2DE92C6F,0x4A7484AA,0x5CB0A9DC,0x76F988DA,0x983E5152,0xA831C66D,0xB00327C8,0xBF597FC7,0xC6E00BF3,0xD5A79147,0x6CA6351,0x14292967,0x27B70A85,0x2E1B2138,0x4D2C6DFC,0x53380D13,0x650A7354,0x766A0ABB,0x81C2C92E,0x92722C85,0xA2BFE8A1,0xA81A664B,0xC24B8B70,0xC76C51A3,0xD192E819,0xD6990624,0xF40E3585,0x106AA070,0x19A4C116,0x1E376C08,0x2748774C,0x34B0BCB5,0x391C0CB3,0x4ED8AA4A,0x5B9CCA4F,0x682E6FF3,0x748F82EE,0x78A5636F,0x84C87814,0x8CC70208,0x90BEFFFA,0xA4506CEB,0xBEF9A3F7,0xC67178F2];
var HASH = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19];
var W = newArray(64);
var a, b, c, d, e, f, g, h, i, j;
var T1, T2;
/* append padding */
m[l >> 5] |= 0x80 << (24 - l % 32);
m[((l + 64 >> 9) << 4) + 15] = l;
for ( var i = 0; i<m.length; i+=16 ) {
a = HASH[0]; b = HASH[1]; c = HASH[2]; d = HASH[3];
e = HASH[4]; f = HASH[5]; g = HASH[6]; h = HASH[7];
for ( var j = 0; j<64; j++) {
if (j < 16) {
W[j] = m[j + i];
} else {
W[j] = safe_add(safe_add(safe_add(Gamma1256(
W[j - 2]), W[j - 7]), Gamma0256(W[j - 15])), W[j - 16]);
}
T1 = safe_add(safe_add(safe_add(
safe_add(h, Sigma1256(e)), Ch(e, f, g)), K[j]), W[j]);
T2 = safe_add(Sigma0256(a), Maj(a, b, c));
h = g; g = f; f = e; e = safe_add(d, T1);
d = c; c = b; b = a; a = safe_add(T1, T2);
}
HASH[0] = safe_add(a, HASH[0]); HASH[1] = safe_add(b, HASH[1]);
HASH[2] = safe_add(c, HASH[2]); HASH[3] = safe_add(d, HASH[3]);
HASH[4] = safe_add(e, HASH[4]); HASH[5] = safe_add(f, HASH[5]);
HASH[6] = safe_add(g, HASH[6]); HASH[7] = safe_add(h, HASH[7]);
}
return HASH;
}
function str2binb (str) {
var bin = Array();
var mask = (1 << chrsz) - 1;
for(var i = 0; i < str.length * chrsz; i += chrsz)
bin[i>>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
return bin;
}
function binb2hex (binarray) {
var hexcase = 0; /* hex output format. 0 - lowercase; 1 - uppercase */
var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
var str = "";
for (var i = 0; i < binarray.length * 4; i++) {
str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
}
return str;
}
function hex_sha256(s){
return binb2hex(core_sha256(str2binb(s),s.length * chrsz));
}
window.hex_sha256 = hex_sha256;
}());
}));
(function(globals) {
var define, requireModule;
(function() {
var registry = {}, seen = {};
define = function(name, deps, callback) {
registry[name] = { deps: deps, callback: callback };
};
requireModule = function(name) {
if (seen[name]) { return seen[name]; }
seen[name] = {};
var mod = registry[name];
if (!mod) {
throw new Error("Module '" + name + "' not found.");
}
var deps = mod.deps,
callback = mod.callback,
reified = [],
exports;
for (var i=0, l=deps.length; i<l; i++) {
if (deps[i] === 'exports') {
reified.push(exports = {});
} else {
reified.push(requireModule(deps[i]));
}
}
var value = callback.apply(this, reified);
return seen[name] = exports || value;
};
})();
define("rsvp/all",
["rsvp/promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
/* global toString */
function promiseAtLeast(expected_count, promises) {
if (Object.prototype.toString.call(promises) !== "[object Array]") {
throw new TypeError('You must pass an array to all.');
}
function canceller() {
var promise;
for (var i = 0; i < promises.length; i++) {
promise = promises[i];
if (promise && typeof promise.then === 'function' &&
typeof promise.cancel === 'function') {
promise.cancel();
}
}
}
return new Promise(function(resolve, reject, notify) {
var results = [], remaining = promises.length,
promise, remaining_count = promises.length - expected_count;
if (remaining === 0) {
if (expected_count === 1) {
resolve();
} else {
resolve([]);
}
}
function resolver(index) {
return function(value) {
resolveAll(index, value);
};
}
function resolveAll(index, value) {
results[index] = value;
if (--remaining === remaining_count) {
if (remaining_count === 0) {
resolve(results);
} else {
resolve(value);
canceller();
}
}
}
function notifier(index) {
return function(value) {
notify({"index": index, "value": value});
};
}
function cancelAll(rejectionValue) {
reject(rejectionValue);
canceller();
}
for (var i = 0; i < promises.length; i++) {
promise = promises[i];
if (promise && typeof promise.then === 'function') {
promise.then(resolver(i), cancelAll, notifier(i));
} else {
resolveAll(i, promise);
}
}
}, canceller
);
}
function all(promises) {
return promiseAtLeast(promises.length, promises);
}
function any(promises) {
return promiseAtLeast(1, promises);
}
__exports__.all = all;
__exports__.any = any;
});
define("rsvp/async",
["exports"],
function(__exports__) {
"use strict";
var browserGlobal = (typeof window !== 'undefined') ? window : {};
var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
var async;
var local = (typeof global !== 'undefined') ? global : this;
// old node
function useNextTick() {
return function(callback, arg) {
process.nextTick(function() {
callback(arg);
});
};
}
// node >= 0.10.x
function useSetImmediate() {
return function(callback, arg) {
/* global setImmediate */
setImmediate(function(){
callback(arg);
});
};
}
function useMutationObserver() {
var queue = [];
var observer = new BrowserMutationObserver(function() {
var toProcess = queue.slice();
queue = [];
toProcess.forEach(function(tuple) {
var callback = tuple[0], arg= tuple[1];
callback(arg);
});
});
var element = document.createElement('div');
observer.observe(element, { attributes: true });
// Chrome Memory Leak: https://bugs.webkit.org/show_bug.cgi?id=93661
window.addEventListener('unload', function(){
observer.disconnect();
observer = null;
}, false);
return function(callback, arg) {
queue.push([callback, arg]);
element.setAttribute('drainQueue', 'drainQueue');
};
}
function useSetTimeout() {
return function(callback, arg) {
local.setTimeout(function() {
callback(arg);
}, 1);
};
}
if (typeof setImmediate === 'function') {
async = useSetImmediate();
} else if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
async = useNextTick();
} else if (BrowserMutationObserver) {
async = useMutationObserver();
} else {
async = useSetTimeout();
}
__exports__.async = async;
});
define("rsvp/cancellation_error",
["exports"],
function(__exports__) {
"use strict";
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
function CancellationError(message) {
this.name = "cancel";
if ((message !== undefined) && (typeof message !== "string")) {
throw new TypeError('You must pass a string.');
}
this.message = message || "Default Message";
}
CancellationError.prototype = new Error();
CancellationError.prototype.constructor = CancellationError;
__exports__.CancellationError = CancellationError;
});
define("rsvp/config",
["rsvp/async","exports"],
function(__dependency1__, __exports__) {
"use strict";
var async = __dependency1__.async;
var config = {};
config.async = async;
__exports__.config = config;
});
define("rsvp/defer",
["rsvp/promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
function defer() {
var deferred = {
// pre-allocate shape
resolve: undefined,
reject: undefined,
promise: undefined
};
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
});
return deferred;
}
__exports__.defer = defer;
});
define("rsvp/events",
["exports"],
function(__exports__) {
"use strict";
var Event = function(type, options) {
this.type = type;
for (var option in options) {
if (!options.hasOwnProperty(option)) { continue; }
this[option] = options[option];
}
};
var indexOf = function(callbacks, callback) {
for (var i=0, l=callbacks.length; i<l; i++) {
if (callbacks[i][0] === callback) { return i; }
}
return -1;
};
var callbacksFor = function(object) {
var callbacks = object._promiseCallbacks;
if (!callbacks) {
callbacks = object._promiseCallbacks = {};
}
return callbacks;
};
var EventTarget = {
mixin: function(object) {
object.on = this.on;
object.off = this.off;
object.trigger = this.trigger;
return object;
},
on: function(eventNames, callback, binding) {
var allCallbacks = callbacksFor(this), callbacks, eventName;
eventNames = eventNames.split(/\s+/);
binding = binding || this;
while (eventName = eventNames.shift()) {
callbacks = allCallbacks[eventName];
if (!callbacks) {
callbacks = allCallbacks[eventName] = [];
}
if (indexOf(callbacks, callback) === -1) {
callbacks.push([callback, binding]);
}
}
},
off: function(eventNames, callback) {
var allCallbacks = callbacksFor(this), callbacks, eventName, index;
eventNames = eventNames.split(/\s+/);
while (eventName = eventNames.shift()) {
if (!callback) {
allCallbacks[eventName] = [];
continue;
}
callbacks = allCallbacks[eventName];
index = indexOf(callbacks, callback);
if (index !== -1) { callbacks.splice(index, 1); }
}
},
trigger: function(eventName, options) {
var allCallbacks = callbacksFor(this),
callbacks, callbackTuple, callback, binding, event;
if (callbacks = allCallbacks[eventName]) {
// Don't cache the callbacks.length since it may grow
for (var i=0; i<callbacks.length; i++) {
callbackTuple = callbacks[i];
callback = callbackTuple[0];
binding = callbackTuple[1];
if (typeof options !== 'object') {
options = { detail: options };
}
event = new Event(eventName, options);
callback.call(binding, event);
}
}
}
};
__exports__.EventTarget = EventTarget;
});
define("rsvp/hash",
["rsvp/defer","exports"],
function(__dependency1__, __exports__) {
"use strict";
var defer = __dependency1__.defer;
function size(object) {
var s = 0;
for (var prop in object) {
s++;
}
return s;
}
function hash(promises) {
var results = {}, deferred = defer(), remaining = size(promises);
if (remaining === 0) {
deferred.resolve({});
}
var resolver = function(prop) {
return function(value) {
resolveAll(prop, value);
};
};
var resolveAll = function(prop, value) {
results[prop] = value;
if (--remaining === 0) {
deferred.resolve(results);
}
};
var rejectAll = function(error) {
deferred.reject(error);
};
for (var prop in promises) {
if (promises[prop] && typeof promises[prop].then === 'function') {
promises[prop].then(resolver(prop), rejectAll);
} else {
resolveAll(prop, promises[prop]);
}
}
return deferred.promise;
}
__exports__.hash = hash;
});
define("rsvp/node",
["rsvp/promise","rsvp/all","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
var all = __dependency2__.all;
function makeNodeCallbackFor(resolve, reject) {
return function (error, value) {
if (error) {
reject(error);
} else if (arguments.length > 2) {
resolve(Array.prototype.slice.call(arguments, 1));
} else {
resolve(value);
}
};
}
function denodeify(nodeFunc) {
return function() {
var nodeArgs = Array.prototype.slice.call(arguments), resolve, reject;
var thisArg = this;
var promise = new Promise(function(nodeResolve, nodeReject) {
resolve = nodeResolve;
reject = nodeReject;
});
all(nodeArgs).then(function(nodeArgs) {
nodeArgs.push(makeNodeCallbackFor(resolve, reject));
try {
nodeFunc.apply(thisArg, nodeArgs);
} catch(e) {
reject(e);
}
});
return promise;
};
}
__exports__.denodeify = denodeify;
});
define("rsvp/promise",
["rsvp/config","rsvp/events","rsvp/cancellation_error","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __exports__) {
"use strict";
var config = __dependency1__.config;
var EventTarget = __dependency2__.EventTarget;
var CancellationError = __dependency3__.CancellationError;
function objectOrFunction(x) {
return isFunction(x) || (typeof x === "object" && x !== null);
}
function isFunction(x){
return typeof x === "function";
}
var Promise = function(resolver, canceller) {
var promise = this,
resolved = false;
if (typeof resolver !== 'function') {
throw new TypeError('You must pass a resolver function as the sole argument to the promise constructor');
}
if ((canceller !== undefined) && (typeof canceller !== 'function')) {
throw new TypeError('You can only pass a canceller function' +
' as the second argument to the promise constructor');
}
if (!(promise instanceof Promise)) {
return new Promise(resolver, canceller);
}
var resolvePromise = function(value) {
if (resolved) { return; }
resolved = true;
resolve(promise, value);
};
var rejectPromise = function(value) {
if (resolved) { return; }
resolved = true;
reject(promise, value);
};
var notifyPromise = function(value) {
if (resolved) { return; }
notify(promise, value);
};
this.on('promise:failed', function(event) {
this.trigger('error', { detail: event.detail });
}, this);
this.on('error', onerror);
this.cancel = function () {
// For now, simply reject the promise and does not propagate the cancel
// to parent or children
if (resolved) { return; }
if (canceller !== undefined) {
try {
canceller();
} catch (e) {
rejectPromise(e);
return;
}
}
// Trigger cancel?
rejectPromise(new CancellationError());
};
try {
resolver(resolvePromise, rejectPromise, notifyPromise);
} catch(e) {
rejectPromise(e);
}
};
function onerror(event) {
if (config.onerror) {
config.onerror(event.detail);
}
}
var invokeCallback = function(type, promise, callback, event) {
var hasCallback = isFunction(callback),
value, error, succeeded, failed;
if (promise.isFulfilled) { return; }
if (promise.isRejected) { return; }
if (hasCallback) {
try {
value = callback(event.detail);
succeeded = true;
} catch(e) {
failed = true;
error = e;
}
} else {
value = event.detail;
succeeded = true;
}
if (handleThenable(promise, value)) {
return;
} else if (hasCallback && succeeded) {
resolve(promise, value);
} else if (failed) {
reject(promise, error);
} else if (type === 'resolve') {
resolve(promise, value);
} else if (type === 'reject') {
reject(promise, value);
}
};
var invokeNotifyCallback = function(promise, callback, event) {
var value;
if (typeof callback === 'function') {
try {
value = callback(event.detail);
} catch (e) {
// stop propagating
return;
}
notify(promise, value);
} else {
notify(promise, event.detail);
}
};
Promise.prototype = {
constructor: Promise,
isRejected: undefined,
isFulfilled: undefined,
rejectedReason: undefined,
fulfillmentValue: undefined,
then: function(done, fail, progress) {
this.off('error', onerror);
var thenPromise = new this.constructor(function() {},
function () {
thenPromise.trigger('promise:cancelled', {});
});
if (this.isFulfilled) {
config.async(function(promise) {
invokeCallback('resolve', thenPromise, done, { detail: promise.fulfillmentValue });
}, this);
}
if (this.isRejected) {
config.async(function(promise) {
invokeCallback('reject', thenPromise, fail, { detail: promise.rejectedReason });
}, this);
}
this.on('promise:resolved', function(event) {
invokeCallback('resolve', thenPromise, done, event);
});
this.on('promise:failed', function(event) {
invokeCallback('reject', thenPromise, fail, event);
});
this.on('promise:notified', function (event) {
invokeNotifyCallback(thenPromise, progress, event);
});
return thenPromise;
},
fail: function(fail) {
return this.then(null, fail);
},
always: function(fail) {
return this.then(fail, fail);
}
};
EventTarget.mixin(Promise.prototype);
function resolve(promise, value) {
if (promise === value) {
fulfill(promise, value);
} else if (!handleThenable(promise, value)) {
fulfill(promise, value);
}
}
function handleThenable(promise, value) {
var then = null,
resolved;
try {
if (promise === value) {
throw new TypeError("A promises callback cannot return that same promise.");
}
if (objectOrFunction(value)) {
then = value.then;
if (isFunction(then)) {
promise.on('promise:cancelled', function(event) {
if (isFunction(value.cancel)) {
value.cancel();
}
});
then.call(value, function(val) {
if (resolved) { return true; }
resolved = true;
if (value !== val) {
resolve(promise, val);
} else {
fulfill(promise, val);
}
}, function(val) {
if (resolved) { return true; }
resolved = true;
reject(promise, val);
});
return true;
}
}
} catch (error) {
reject(promise, error);
return true;
}
return false;
}
function fulfill(promise, value) {
config.async(function() {
if (promise.isFulfilled) { return; }
if (promise.isRejected) { return; }
promise.trigger('promise:resolved', { detail: value });
promise.isFulfilled = true;
promise.fulfillmentValue = value;
});
}
function reject(promise, value) {
config.async(function() {
if (promise.isFulfilled) { return; }
if (promise.isRejected) { return; }
promise.trigger('promise:failed', { detail: value });
promise.isRejected = true;
promise.rejectedReason = value;
});
}
function notify(promise, value) {
config.async(function() {
promise.trigger('promise:notified', { detail: value });
});
}
__exports__.Promise = Promise;
});
define("rsvp/queue",
["rsvp/promise","rsvp/timeout","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
var delay = __dependency2__.delay;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
function ResolvedQueueError(message) {
this.name = "resolved";
if ((message !== undefined) && (typeof message !== "string")) {
throw new TypeError('You must pass a string.');
}
this.message = message || "Default Message";
}
ResolvedQueueError.prototype = new Error();
ResolvedQueueError.prototype.constructor = ResolvedQueueError;
var Queue = function() {
var queue = this,
promise_list = [],
promise,
fulfill,
reject,
resolved;
if (!(this instanceof Queue)) {
return new Queue();
}
function canceller() {
for (var i = 0; i < 2; i++) {
promise_list[i].cancel();
}
}
promise = new Promise(function(done, fail) {
fulfill = function (fulfillmentValue) {
if (resolved) {return;}
queue.isFulfilled = true;
queue.fulfillmentValue = fulfillmentValue;
resolved = true;
return done(fulfillmentValue);
};
reject = function (rejectedReason) {
if (resolved) {return;}
queue.isRejected = true;
queue.rejectedReason = rejectedReason ;
resolved = true;
return fail(rejectedReason);
};
}, canceller);
promise_list.push(delay());
promise_list.push(promise_list[0].then(function () {
promise_list.splice(0, 2);
if (promise_list.length === 0) {
fulfill();
}
}));
queue.cancel = function () {
if (resolved) {return;}
resolved = true;
promise.cancel();
promise.fail(function (rejectedReason) {
queue.isRejected = true;
queue.rejectedReason = rejectedReason;
});
};
queue.then = function () {
return promise.then.apply(promise, arguments);
};
queue.push = function(done, fail) {
var last_promise = promise_list[promise_list.length - 1],
next_promise;
if (resolved) {
throw new ResolvedQueueError();
}
next_promise = last_promise.then(done, fail);
promise_list.push(next_promise);
// Handle pop
promise_list.push(next_promise.then(function (fulfillmentValue) {
promise_list.splice(0, 2);
if (promise_list.length === 0) {
fulfill(fulfillmentValue);
} else {
return fulfillmentValue;
}
}, function (rejectedReason) {
promise_list.splice(0, 2);
if (promise_list.length === 0) {
reject(rejectedReason);
} else {
throw rejectedReason;
}
}));
return this;
};
};
Queue.prototype = Object.create(Promise.prototype);
Queue.prototype.constructor = Queue;
__exports__.Queue = Queue;
__exports__.ResolvedQueueError = ResolvedQueueError;
});
define("rsvp/reject",
["rsvp/promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
function reject(reason) {
return new Promise(function (resolve, reject) {
reject(reason);
});
}
__exports__.reject = reject;
});
define("rsvp/resolve",
["rsvp/promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
function resolve(thenable) {
return new Promise(function(resolve, reject) {
if (typeof thenable === "object" && thenable !== null) {
var then = thenable.then;
if ((then !== undefined) && (typeof then === "function")) {
return then.apply(thenable, [resolve, reject]);
}
}
return resolve(thenable);
}, function () {
if ((thenable !== undefined) && (thenable.cancel !== undefined)) {
thenable.cancel();
}
});
}
__exports__.resolve = resolve;
});
define("rsvp/rethrow",
["exports"],
function(__exports__) {
"use strict";
var local = (typeof global === "undefined") ? this : global;
function rethrow(reason) {
local.setTimeout(function() {
throw reason;
});
throw reason;
}
__exports__.rethrow = rethrow;
});
define("rsvp/timeout",
["rsvp/promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__.Promise;
function promiseSetTimeout(millisecond, should_reject, message) {
var timeout_id;
function resolver(resolve, reject) {
timeout_id = setTimeout(function () {
if (should_reject) {
reject(message);
} else {
resolve(message);
}
}, millisecond);
}
function canceller() {
clearTimeout(timeout_id);
}
return new Promise(resolver, canceller);
}
function delay(millisecond, message) {
return promiseSetTimeout(millisecond, false, message);
}
function timeout(millisecond) {
return promiseSetTimeout(millisecond, true,
"Timed out after " + millisecond + " ms");
}
Promise.prototype.delay = function(millisecond) {
return this.then(function (fulfillmentValue) {
return delay(millisecond, fulfillmentValue);
});
};
__exports__.delay = delay;
__exports__.timeout = timeout;
});
define("rsvp",
["rsvp/events","rsvp/cancellation_error","rsvp/promise","rsvp/node","rsvp/all","rsvp/queue","rsvp/timeout","rsvp/hash","rsvp/rethrow","rsvp/defer","rsvp/config","rsvp/resolve","rsvp/reject","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __exports__) {
"use strict";
var EventTarget = __dependency1__.EventTarget;
var CancellationError = __dependency2__.CancellationError;
var Promise = __dependency3__.Promise;
var denodeify = __dependency4__.denodeify;
var all = __dependency5__.all;
var any = __dependency5__.any;
var Queue = __dependency6__.Queue;
var ResolvedQueueError = __dependency6__.ResolvedQueueError;
var delay = __dependency7__.delay;
var timeout = __dependency7__.timeout;
var hash = __dependency8__.hash;
var rethrow = __dependency9__.rethrow;
var defer = __dependency10__.defer;
var config = __dependency11__.config;
var resolve = __dependency12__.resolve;
var reject = __dependency13__.reject;
function configure(name, value) {
config[name] = value;
}
__exports__.CancellationError = CancellationError;
__exports__.Promise = Promise;
__exports__.EventTarget = EventTarget;
__exports__.all = all;
__exports__.any = any;
__exports__.Queue = Queue;
__exports__.ResolvedQueueError = ResolvedQueueError;
__exports__.delay = delay;
__exports__.timeout = timeout;
__exports__.hash = hash;
__exports__.rethrow = rethrow;
__exports__.defer = defer;
__exports__.denodeify = denodeify;
__exports__.configure = configure;
__exports__.resolve = resolve;
__exports__.reject = reject;
});
window.RSVP = requireModule("rsvp");
})(window);
\ No newline at end of file
/**
* sinon-qunit 1.0.0, 2010/12/09
*
* @author Christian Johansen (christian@cjohansen.no)
*
* (The BSD License)
*
* Copyright (c) 2010-2011, Christian Johansen, christian@cjohansen.no
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
* * Neither the name of Christian Johansen nor the names of his contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/*global sinon, QUnit, test*/
sinon.assert.fail = function (msg) {
QUnit.ok(false, msg);
};
sinon.assert.pass = function (assertion) {
QUnit.ok(true, assertion);
};
sinon.config = {
injectIntoThis: true,
injectInto: null,
properties: ["spy", "stub", "mock", "clock", "sandbox"],
useFakeTimers: true,
useFakeServer: false
};
(function (global) {
var qTest = QUnit.test;
QUnit.test = global.test = function (testName, expected, callback, async) {
if (arguments.length === 2) {
callback = expected;
expected = null;
}
return qTest(testName, expected, sinon.test(callback), async);
};
}(this));
/*! RenderJs v0.2 */ /*! RenderJs v0.3 */
/*global jQuery, window, document, DOMParser, Channel */ /*global RSVP, window, document, DOMParser, Channel, XMLHttpRequest, alert */
/*jslint unparam: true, maxlen: 150 */
"use strict"; "use strict";
/* /*
...@@ -22,7 +23,7 @@ ...@@ -22,7 +23,7 @@
// text/html parsing is natively supported // text/html parsing is natively supported
return; return;
} }
} catch (ex) {} } catch (ex) {console.warn(ex); }
DOMParser_proto.parseFromString = function (markup, type) { DOMParser_proto.parseFromString = function (markup, type) {
var result, doc, doc_elt, first_elt; var result, doc, doc_elt, first_elt;
...@@ -50,17 +51,20 @@ ...@@ -50,17 +51,20 @@
* renderJs - Generic Gadget library renderer. * renderJs - Generic Gadget library renderer.
* http://www.renderjs.org/documentation * http://www.renderjs.org/documentation
*/ */
(function (document, window, $, DOMParser, Channel, undefined) { (function (document, window, RSVP, DOMParser, Channel, undefined) {
var gadget_model_dict = {}, var gadget_model_dict = {},
javascript_registration_dict = {}, javascript_registration_dict = {},
stylesheet_registration_dict = {}, stylesheet_registration_dict = {},
gadget_loading_klass, gadget_loading_klass,
methods,
loading_gadget_promise, loading_gadget_promise,
renderJS; renderJS;
function RenderJSGadget() {} function RenderJSGadget() {
if (!(this instanceof RenderJSGadget)) {
return new RenderJSGadget();
}
}
RenderJSGadget.prototype.title = ""; RenderJSGadget.prototype.title = "";
RenderJSGadget.prototype.interface_list = []; RenderJSGadget.prototype.interface_list = [];
RenderJSGadget.prototype.path = ""; RenderJSGadget.prototype.path = "";
...@@ -75,36 +79,14 @@ ...@@ -75,36 +79,14 @@
}; };
RenderJSGadget.declareMethod = function (name, callback) { RenderJSGadget.declareMethod = function (name, callback) {
// // Register the potentially loading javascript
// var script_element = $('script').last(),
// src = script_element.attr('src');
// if (src !== undefined) {
// if (javascript_registration_dict[src] === undefined) {
// // First time loading the JS file.
// // Remember all declareMethod calls
// javascript_registration_dict[src] = {
// loaded: false,
// method_list: [[name, callback]],
// };
// script_element.load(function () {
// javascript_registration_dict[src].loaded = true;
// });
// } else if (!javascript_registration_dict[src].loaded) {
// javascript_registration_dict[src].method_list.push([name, callback]);
// }
// }
this.prototype[name] = function () { this.prototype[name] = function () {
var dfr = $.Deferred(), var context = this,
gadget = this; argument_list = arguments;
$.when(callback.apply(this, arguments))
.done(function () { return new RSVP.Queue()
dfr.resolveWith(gadget, arguments); .push(function () {
}) return callback.apply(context, argument_list);
.fail(function () {
dfr.rejectWith(gadget, arguments);
}); });
return dfr.promise();
}; };
// Allow chain // Allow chain
return this; return this;
...@@ -131,400 +113,101 @@ ...@@ -131,400 +113,101 @@
// Returns the title of a gadget // Returns the title of a gadget
return this.title; return this.title;
}) })
.declareMethod('getHTML', function () { .declareMethod('getElement', function () {
// Returns the HTML of a gadget // Returns the DOM Element of a gadget
return this.html; if (this.element === undefined) {
throw new Error("No element defined");
}
return this.element;
}); });
// Class inheritance RenderJSGadget.prototype.declareGadget = function (url, options) {
function RenderJSEmbeddedGadget() { var gadget_instance,
var root_gadget = this, queue,
declare_method_count = 0, previous_loading_gadget_promise = loading_gadget_promise;
gadget_ready = false,
// Create the communication channel
embedded_channel = Channel.build({
window: window.parent,
origin: "*",
scope: "renderJS"
});
RenderJSGadget.call(this);
// Bind calls to renderJS method on the instance
embedded_channel.bind("methodCall", function (trans, v) {
root_gadget[v[0]].apply(root_gadget, v[1]).done(function (g) {
trans.complete(g);
}).fail(function () {
trans.error(Array.prototype.slice.call(arguments, 0));
});
trans.delayReturn(true);
});
// Notify parent about gadget instanciation if (options === undefined) {
function notifyReady() { options = {};
if ((declare_method_count === 0) && (gadget_ready === true)) {
embedded_channel.notify({method: "ready"});
}
} }
if (options.element === undefined) {
// Inform parent gadget about declareMethod calls here. options.element = document.createElement("div");
function notifyDeclareMethod(name) {
declare_method_count += 1;
embedded_channel.call({
method: "declareMethod",
params: name,
success: function () {
declare_method_count -= 1;
notifyReady();
},
error: function () {
declare_method_count -= 1;
// console.error(Array.prototype.slice.call(arguments, 0));
},
});
} }
notifyDeclareMethod("getInterfaceList");
notifyDeclareMethod("getRequiredCSSList");
notifyDeclareMethod("getRequiredJSList");
notifyDeclareMethod("getPath");
notifyDeclareMethod("getTitle");
notifyDeclareMethod("getHTML");
// Surcharge declareMethod to inform parent window
this.constructor.declareMethod = function (name, callback) {
notifyDeclareMethod(name);
return RenderJSGadget.declareMethod.apply(this, [name, callback]);
};
// Inform parent window that gadget is correctly loaded
loading_gadget_promise.done(function () {
gadget_ready = true;
notifyReady();
}).fail(function () {
embedded_channel.notify({method: "failed"});
});
return root_gadget;
}
RenderJSEmbeddedGadget.ready_list = [];
RenderJSEmbeddedGadget.ready =
RenderJSGadget.ready;
RenderJSEmbeddedGadget.prototype = new RenderJSGadget();
RenderJSEmbeddedGadget.prototype.constructor = RenderJSEmbeddedGadget;
// Class inheritance
function RenderJSIframeGadget() {
RenderJSGadget.call(this);
}
RenderJSIframeGadget.ready_list = [];
RenderJSIframeGadget.declareMethod =
RenderJSGadget.declareMethod;
RenderJSIframeGadget.ready =
RenderJSGadget.ready;
RenderJSIframeGadget.prototype = new RenderJSGadget();
RenderJSIframeGadget.prototype.constructor = RenderJSIframeGadget;
RenderJSGadget.prototype.declareIframedGadget =
function (url, jquery_context) {
var previous_loading_gadget_promise = loading_gadget_promise,
next_loading_gadget_deferred = $.Deferred();
// Change the global variable to update the loading queue
loading_gadget_promise = next_loading_gadget_deferred.promise();
// Wait for previous gadget loading to finish first
previous_loading_gadget_promise.always(function () {
// Instanciate iframe
var gadget = new RenderJSIframeGadget();
gadget.context = jquery_context;
// XXX Do not set this info on the instance!
gadget.path = url;
// XXX onload onerror
// $('iframe').load(function() {
// RunAfterIFrameLoaded();
// });
// Create the iframe
if (gadget.context !== undefined) {
$(gadget.context).html(
// Use encodeURI to prevent XSS
'<iframe src="' + encodeURI(url) + '"></iframe>'
);
gadget.chan = Channel.build({
window: gadget.context.find('iframe').first()[0].contentWindow,
origin: "*",
scope: "renderJS"
});
// gadget.getTitle = function () {
// var dfr = $.Deferred();
// gadget.chan.call({
// method: "getTitle",
// success: function (v) {
// dfr.resolve(v);
// }
// });
// return dfr.promise();
// };
gadget.chan.bind("declareMethod", function (trans, method_name) {
gadget[method_name] = function () {
var dfr = $.Deferred();
gadget.chan.call({
method: "methodCall",
params: [
method_name,
Array.prototype.slice.call(arguments, 0)],
success: function () {
dfr.resolveWith(gadget, arguments);
},
error: function () {
dfr.rejectWith(gadget, arguments);
}
// XXX Error callback
});
return dfr.promise();
};
return "OK";
});
// Wait for the iframe to be loaded before continuing
gadget.chan.bind("ready", function (trans) {
next_loading_gadget_deferred.resolve(gadget);
return "OK";
});
gadget.chan.bind("failed", function (trans) {
next_loading_gadget_deferred.reject();
return "OK";
});
} else {
next_loading_gadget_deferred.reject();
}
});
loading_gadget_promise
// Drop the current loading klass info used by selector
.done(function () {
gadget_loading_klass = undefined;
})
.fail(function () {
gadget_loading_klass = undefined;
})
.done(function (created_gadget) {
$.each(created_gadget.constructor.ready_list,
function (i, callback) {
callback.apply(created_gadget);
});
});
return loading_gadget_promise;
};
RenderJSGadget.prototype.declareGadget = function (url, jquery_context) {
var previous_loading_gadget_promise = loading_gadget_promise,
next_loading_gadget_deferred = $.Deferred();
// Change the global variable to update the loading queue // Change the global variable to update the loading queue
loading_gadget_promise = next_loading_gadget_deferred.promise(); queue = new RSVP.Queue()
// Wait for previous gadget loading to finish first
// Wait for previous gadget loading to finish first .push(function () {
previous_loading_gadget_promise.always(function () { return previous_loading_gadget_promise;
})
.push(function () {
return renderJS.declareGadgetKlass(url);
})
// Get the gadget class and instanciate it // Get the gadget class and instanciate it
renderJS.declareGadgetKlass(url).done(function (Klass) { .push(function (Klass) {
var gadget = new Klass(); var i,
gadget.context = jquery_context; template_node_list = Klass.template_element.body.childNodes;
gadget_loading_klass = Klass;
gadget_instance = new Klass();
gadget_instance.element = options.element;
for (i = 0; i < template_node_list.length; i += 1) {
gadget_instance.element.appendChild(template_node_list[i].cloneNode(true));
}
// Load dependencies if needed // Load dependencies if needed
$.when(gadget.getRequiredJSList(), gadget.getRequiredCSSList()) return RSVP.all([
.done(function (js_list, css_list) { gadget_instance.getRequiredJSList(),
var result_list = [], gadget_instance.getRequiredCSSList()
first_deferred = $.Deferred(), ]);
first_promise = first_deferred.promise();
gadget_loading_klass = Klass;
// Load JS and follow the dependency declaration defined in the
// head
function next(next_js_list) {
var next_js = next_js_list.shift();
if (next_js === undefined) {
first_deferred.resolve();
} else {
renderJS.declareJS(next_js)
.done(function () {
next(next_js_list);
})
.fail(function () {
first_deferred.reject.apply(
first_deferred,
arguments
);
});
}
}
next(js_list);
result_list.push(first_promise);
// Load CSS
$.each(css_list, function (i, required_url) {
result_list.push(renderJS.declareCSS(required_url));
});
$.when.apply(this, result_list)
.done(function () {
// Dependency correctly loaded. Fire instanciation success.
next_loading_gadget_deferred.resolve(gadget);
}).fail(function () {
// console.error(Array.prototype.slice.call(arguments, 0));
// One error during css/js loading
next_loading_gadget_deferred.reject.apply(
next_loading_gadget_deferred,
arguments
);
});
}).fail(function () {
// Failed to fetch dependencies information.
next_loading_gadget_deferred.reject.apply(
next_loading_gadget_deferred,
arguments
);
});
}).fail(function () {
// Klass not correctly loaded. Reject instanciation
next_loading_gadget_deferred.reject.apply(next_loading_gadget_deferred,
arguments);
});
});
loading_gadget_promise
// Drop the current loading klass info used by selector
.done(function () {
gadget_loading_klass = undefined;
}) })
.fail(function () { // Load all JS/CSS
gadget_loading_klass = undefined; .push(function (all_list) {
var parameter_list = [],
i;
// Load JS
for (i = 0; i < all_list[0].length; i += 1) {
parameter_list.push(renderJS.declareJS(all_list[0][i]));
}
// Load CSS
for (i = 0; i < all_list[1].length; i += 1) {
parameter_list.push(renderJS.declareCSS(all_list[1][i]));
}
return RSVP.all(parameter_list);
}) })
.done(function (created_gadget) { // Set the HTML context
// Set the content html and call the ready list if instance is .push(function () {
// correctly loaded var i;
if (created_gadget.context !== undefined) { // Drop the current loading klass info used by selector
$(created_gadget.context).html( gadget_loading_klass = undefined;
created_gadget.constructor.prototype.html // Trigger calling of all ready callback
); function ready_wrapper() {
return gadget_instance;
} }
$.each(created_gadget.constructor.ready_list, function (i, callback) { for (i = 0; i < gadget_instance.constructor.ready_list.length;
callback.apply(created_gadget); i += 1) {
}); // Put a timeout?
queue.push(gadget_instance.constructor.ready_list[i]);
// Always return the gadget instance after ready function
queue.push(ready_wrapper);
}
return gadget_instance;
})
.push(undefined, function (e) {
// Drop the current loading klass info used by selector
// even in case of error
gadget_loading_klass = undefined;
console.warn("failed to declare " + url);
console.warn(e);
throw e;
}); });
loading_gadget_promise = queue;
return loading_gadget_promise; return loading_gadget_promise;
}; };
methods = {
loadGadgetFromDom: function () {
$(this).find('[data-gadget-path]').each(function (index, value) {
$(this).renderJS('declareGadget', $(this).attr('data-gadget-path'), {
scope: $(this).attr('data-gadget-scope'),
})
.done(function (value) {
var parsed_xml;
// Check that context is still attached to the DOM
// XXX Usefull?
if ($(this).closest(document.body).length) {
parsed_xml = $($.parseXML(value));
// Inject the css
// XXX Manage relative URL
$.each(parsed_xml.find('link[rel=stylesheet]'),
function (i, link) {
$('head').append(
'<link rel="stylesheet" href="' +
$(link).attr('href') +
'" type="text/css" />'
);
});
// Inject the js
// XXX Manage relative URL
$.each(parsed_xml.find('script[type="text/javascript"]'),
function (i, script) {
// $('head').append(
// '<script type="text/javascript" href="' +
// $(script).attr('src') +
// '" />'
// );
// Prevent infinite recursion if loading render.js
// more than once
if ($('head').find('script[src="' + $(script).attr('src')
+ '"]').length === 0) {
var headID = document.getElementsByTagName("head")[0],
newScript = document.createElement('script');
newScript.type = 'text/javascript';
newScript.src = $(script).attr('src');
headID.appendChild(newScript);
}
});
// Inject the html
// XXX parseXML does not support <div /> (without 2 tags)
$(this).html(parsed_xml.find('body').clone());
// XXX No idea why it is required to make it work
// Probably because of parseXML
$(this).html($(this).html())
.renderJS('loadGadgetFromDom');
}
});
});
},
};
// // Define a local copy of renderJS
// renderJS = function (selector) {
// // The renderJS object is actually just the init constructor 'enhanced'
// return new renderJS.fn.init(selector, rootrenderJS);
// };
// renderJS.fn = renderJS.prototype = {
// constructor: renderJS,
// init: function (selector, rootrenderJS) {
// var result;
// // HANDLE: $(""), $(null), $(undefined), $(false)
// if (!selector) {
// console.log("no selector");
// result = this;
// // // HANDLE: $(DOMElement)
// // } else if (selector.nodeType) {
// // this.context = this[0] = selector;
// // this.length = 1;
// // result = this;
// // } else if (selector === this) {
// // result = this.constructor();
// } else {
// // throw new Error("Not implemented selector " + selector);
// result = this.constructor();
// }
// return result;
// },
// };
// // Give the init function the renderJS prototype for later instantiation
// renderJS.fn.init.prototype = renderJS.fn;
//
// jQuery.fn.extend({
// attr: function (name, value) {
// return jQuery.access(this, jQuery.attr, name, value,
// arguments.length > 1);
// },
// });
renderJS = function (selector) { renderJS = function (selector) {
var result; var result;
// if (selector.nodeType) {
// console.log(selector);
// } else {
if (selector === window) { if (selector === window) {
// window is the this value when loading a javascript file // window is the 'this' value when loading a javascript file
// In this case, use the current loading gadget constructor // In this case, use the current loading gadget constructor
result = gadget_loading_klass; result = gadget_loading_klass;
// } else if ($.isFunction(selector)) {
// console.log(selector);
} else if (selector instanceof RenderJSGadget) { } else if (selector instanceof RenderJSGadget) {
result = selector; result = selector;
} }
...@@ -535,38 +218,28 @@ ...@@ -535,38 +218,28 @@
}; };
renderJS.declareJS = function (url) { renderJS.declareJS = function (url) {
// // Prevent infinite recursion if loading render.js // Prevent infinite recursion if loading render.js
// // more than once // more than once
// if ($('head').find('script[src="' + $(script).attr('src') var result;
// + '"]').length === 0) {
// var headID = document.getElementsByTagName("head")[0],
// newScript = document.createElement('script');
// newScript.type = 'text/javascript';
// newScript.src = $(script).attr('src');
// headID.appendChild(newScript);
// }
var dfr,
origin_dfr = $.Deferred(),
head_element,
script_element;
if (javascript_registration_dict.hasOwnProperty(url)) { if (javascript_registration_dict.hasOwnProperty(url)) {
setTimeout(function () { result = RSVP.resolve();
origin_dfr.resolve();
});
dfr = origin_dfr.promise();
} else { } else {
dfr = $.ajax({ result = new RSVP.Promise(function (resolve, reject) {
url: url, var newScript;
dataType: "script", newScript = document.createElement('script');
cache: true, newScript.type = 'text/javascript';
}).done(function (script, textStatus) { newScript.src = url;
javascript_registration_dict[url] = null; newScript.onload = function () {
// }).fail(function () { javascript_registration_dict[url] = null;
// console.error(Array.prototype.slice.call(arguments, 0)); resolve();
};
newScript.onerror = function (e) {
reject(e);
};
document.head.appendChild(newScript);
}); });
} }
return dfr; return result;
}; };
renderJS.declareCSS = function (url) { renderJS.declareCSS = function (url) {
...@@ -574,86 +247,116 @@ ...@@ -574,86 +247,116 @@
// No way to cleanly check if a css has been loaded // No way to cleanly check if a css has been loaded
// So, always resolve the promise... // So, always resolve the promise...
// http://requirejs.org/docs/faq-advanced.html#css // http://requirejs.org/docs/faq-advanced.html#css
var origin_dfr = $.Deferred(), var result;
origin_promise = origin_dfr.promise(),
head,
link;
if (stylesheet_registration_dict.hasOwnProperty(url)) { if (stylesheet_registration_dict.hasOwnProperty(url)) {
setTimeout(function () { result = RSVP.resolve();
origin_dfr.resolve();
});
} else { } else {
head = document.getElementsByTagName('head')[0]; result = new RSVP.Promise(function (resolve, reject) {
link = document.createElement('link'); var link;
link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
link.onload = function () {
stylesheet_registration_dict[url] = null;
resolve();
};
link.onerror = function (e) {
reject(e);
};
document.head.appendChild(link);
});
}
return result;
};
link.rel = 'stylesheet'; renderJS.declareGadgetKlass = function (url) {
link.type = 'text/css'; var result,
link.href = url; xhr;
function parse() {
var tmp_constructor,
key,
parsed_html;
if (!gadget_model_dict.hasOwnProperty(url)) {
// Class inheritance
tmp_constructor = function () {
RenderJSGadget.call(this);
};
tmp_constructor.ready_list = [];
tmp_constructor.declareMethod =
RenderJSGadget.declareMethod;
tmp_constructor.ready =
RenderJSGadget.ready;
tmp_constructor.prototype = new RenderJSGadget();
tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.path = url;
// 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
tmp_constructor.template_element =
(new DOMParser()).parseFromString(xhr.responseText, "text/html");
parsed_html = renderJS.parseGadgetHTMLDocument(
tmp_constructor.template_element
);
for (key in parsed_html) {
if (parsed_html.hasOwnProperty(key)) {
tmp_constructor.prototype[key] = parsed_html[key];
}
}
origin_promise.done(function () { gadget_model_dict[url] = tmp_constructor;
stylesheet_registration_dict[url] = null; }
});
head.appendChild(link); return gadget_model_dict[url];
}
setTimeout(function () { function resolver(resolve, reject) {
origin_dfr.resolve(); function handler() {
}); var tmp_result;
try {
if (xhr.readyState === 0) {
// UNSENT
reject(xhr);
} else if (xhr.readyState === 4) {
// DONE
if ((xhr.status < 200) || (xhr.status >= 300) ||
(!/^text\/html[;]?/.test(
xhr.getResponseHeader("Content-Type") || ""
))) {
reject(xhr);
} else {
tmp_result = parse();
resolve(tmp_result);
}
}
} catch (e) {
reject(e);
}
}
xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.onreadystatechange = handler;
xhr.setRequestHeader('Accept', 'text/html');
xhr.withCredentials = true;
xhr.send();
} }
return origin_promise;
};
renderJS.declareGadgetKlass = function (url) { function canceller() {
var dfr = $.Deferred(), if ((xhr !== undefined) && (xhr.readyState !== xhr.DONE)) {
parsed_html; xhr.abort();
}
}
if (gadget_model_dict.hasOwnProperty(url)) { if (gadget_model_dict.hasOwnProperty(url)) {
dfr.resolve(gadget_model_dict[url]); // Return klass object if it already exists
result = RSVP.resolve(gadget_model_dict[url]);
} else { } else {
$.ajax(url) // Fetch the HTML page and parse it
.done(function (value, textStatus, jqXHR) { result = new RSVP.Promise(resolver, canceller);
var klass, tmp_constructor, key;
if (/^text\/html[;]?/.test(
jqXHR.getResponseHeader("Content-Type") || ""
)) {
try {
if (!gadget_model_dict.hasOwnProperty(url)) {
// Class inheritance
tmp_constructor = function () {
RenderJSGadget.call(this);
};
tmp_constructor.ready_list = [];
tmp_constructor.declareMethod =
RenderJSGadget.declareMethod;
tmp_constructor.ready =
RenderJSGadget.ready;
tmp_constructor.prototype = new RenderJSGadget();
tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.path = url;
parsed_html = renderJS.parseGadgetHTML(value);
for (key in parsed_html) {
if (parsed_html.hasOwnProperty(key)) {
tmp_constructor.prototype[key] = parsed_html[key];
}
}
gadget_model_dict[url] = tmp_constructor;
}
dfr.resolve(gadget_model_dict[url]);
} catch (e) {
dfr.reject(jqXHR, "HTML Parsing failed");
}
} else {
dfr.reject(jqXHR, "Unexpected content type");
}
})
.fail(function () {
dfr.reject.apply(dfr, arguments);
});
} }
return dfr.promise(); return result;
}; };
// For test purpose only // For test purpose only
...@@ -663,52 +366,36 @@ ...@@ -663,52 +366,36 @@
stylesheet_registration_dict = {}; stylesheet_registration_dict = {};
}; };
renderJS.parseGadgetHTML = function (html) { renderJS.parseGadgetHTMLDocument = function (document_element) {
var parsed_xml, var settings = {
result,
settings = {
title: "", title: "",
interface_list: [], interface_list: [],
html: "",
required_css_list: [], required_css_list: [],
required_js_list: [], required_js_list: [],
}; },
if (html.constructor === String) { i,
element;
// https://developer.mozilla.org/en-US/docs/HTML_in_XMLHttpRequest if (document_element.nodeType === 9) {
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser settings.title = document_element.title;
// https://developer.mozilla.org/en-US/docs/Code_snippets/HTML_to_DOM
// parsed_xml = $($.parseXML(html)); for (i = 0; i < document_element.head.children.length; i += 1) {
// parsed_xml = $('<div/>').html(html); element = document_element.head.children[i];
parsed_xml = $((new DOMParser()).parseFromString(html, "text/html")); if (element.href !== null) {
settings.title = parsed_xml.find('head > title').first().text(); // XXX Manage relative URL during extraction of URLs
// element.href returns absolute URL in firefox but "" in chrome;
// XXX Manage relative URL during extraction of URLs if (element.rel === "stylesheet") {
$.each(parsed_xml.find('head > link[rel=stylesheet]'), settings.required_css_list.push(element.getAttribute("href"));
function (i, link) { } else if (element.type === "text/javascript") {
settings.required_css_list.push($(link).attr('href')); settings.required_js_list.push(element.getAttribute("src"));
}); } else if (element.rel === "http://www.renderjs.org/rel/interface") {
settings.interface_list.push(element.getAttribute("href"));
$.each(parsed_xml.find('head > script[type="text/javascript"]'), }
function (i, script) { }
settings.required_js_list.push($(script).attr('src'));
});
$.each(parsed_xml.find(
'head > link[rel="http://www.renderjs.org/rel/interface"]'
), function (i, link) {
settings.interface_list.push($(link).attr('href'));
});
settings.html = parsed_xml.find('html > body').first().html();
if (settings.html === undefined) {
settings.html = "";
} }
result = settings;
} else { } else {
throw new Error(html + " is not a string"); throw new Error("The first parameter should be an HTMLDocument");
} }
return result; return settings;
}; };
window.rJS = window.renderJS = renderJS; window.rJS = window.renderJS = renderJS;
window.RenderJSGadget = RenderJSGadget; window.RenderJSGadget = RenderJSGadget;
...@@ -720,76 +407,89 @@ ...@@ -720,76 +407,89 @@
function bootstrap() { function bootstrap() {
var url = window.location.href, var url = window.location.href,
tmp_constructor, tmp_constructor,
root_gadget, root_gadget;
loading_gadget_deferred = $.Deferred();
// Create the gadget class for the current url // Create the gadget class for the current url
if (gadget_model_dict.hasOwnProperty(url)) { if (gadget_model_dict.hasOwnProperty(url)) {
throw new Error("bootstrap should not be called twice"); throw new Error("bootstrap should not be called twice");
} }
loading_gadget_promise = loading_gadget_deferred.promise(); loading_gadget_promise = new RSVP.Promise(function (resolve, reject) {
if (window.self === window.top) {
if (window.self === window.top) { // XXX Copy/Paste from declareGadgetKlass
// XXX Copy/Paste from declareGadgetKlass tmp_constructor = function () {
tmp_constructor = function () { RenderJSGadget.call(this);
RenderJSGadget.call(this); };
}; tmp_constructor.declareMethod = RenderJSGadget.declareMethod;
tmp_constructor.declareMethod = RenderJSGadget.declareMethod; tmp_constructor.ready_list = [];
tmp_constructor.ready_list = []; tmp_constructor.ready = RenderJSGadget.ready;
tmp_constructor.ready = RenderJSGadget.ready; tmp_constructor.prototype = new RenderJSGadget();
tmp_constructor.prototype = new RenderJSGadget(); tmp_constructor.prototype.constructor = tmp_constructor;
tmp_constructor.prototype.constructor = tmp_constructor; tmp_constructor.prototype.path = url;
tmp_constructor.prototype.path = url; gadget_model_dict[url] = tmp_constructor;
gadget_model_dict[url] = tmp_constructor;
// Create the root gadget instance and put it in the loading stack
// Create the root gadget instance and put it in the loading stack root_gadget = new gadget_model_dict[url]();
root_gadget = new gadget_model_dict[url](); }
} else {
// Create the root gadget instance and put it in the loading stack
tmp_constructor = RenderJSEmbeddedGadget;
root_gadget = new RenderJSEmbeddedGadget();
}
gadget_loading_klass = tmp_constructor;
gadget_loading_klass = tmp_constructor;
// run on next tick so that if this was pulled in with requirejs, function init() {
// rJS.ready() can still be used.
// XXX: doesn't work with require(['renderjs', 'somethingElse'], ...
setTimeout(function () {
$(document).ready(function () {
// XXX HTML properties can only be set when the DOM is fully loaded // XXX HTML properties can only be set when the DOM is fully loaded
var settings = renderJS.parseGadgetHTML($('html')[0].outerHTML), var settings = renderJS.parseGadgetHTMLDocument(document),
promise, j,
key; key;
for (key in settings) { for (key in settings) {
if (settings.hasOwnProperty(key)) { if (settings.hasOwnProperty(key)) {
tmp_constructor.prototype[key] = settings[key]; tmp_constructor.prototype[key] = settings[key];
} }
} }
root_gadget.context = $('body'); tmp_constructor.template_element = document.createElement("div");
promise = $.when(root_gadget.getRequiredJSList(), root_gadget.element = document.body;
root_gadget.getRequiredCSSList()) for (j = 0; j < root_gadget.element.childNodes.length; j += 1) {
.done(function (js_list, css_list) { tmp_constructor.template_element.appendChild(
$.each(js_list, function (i, required_url) { root_gadget.element.childNodes[j].cloneNode(true)
javascript_registration_dict[required_url] = null; );
}); }
$.each(css_list, function (i, required_url) { RSVP.all([root_gadget.getRequiredJSList(),
stylesheet_registration_dict[url] = null; root_gadget.getRequiredCSSList()])
}); .then(function (all_list) {
$.each(tmp_constructor.ready_list, function (i, callback) { var i,
callback.apply(root_gadget); js_list = all_list[0],
}); css_list = all_list[1],
queue;
for (i = 0; i < js_list.length; i += 1) {
javascript_registration_dict[js_list[i]] = null;
}
for (i = 0; i < css_list.length; i += 1) {
stylesheet_registration_dict[css_list[i]] = null;
}
gadget_loading_klass = undefined; gadget_loading_klass = undefined;
loading_gadget_deferred.resolve(); queue = new RSVP.Queue();
}).fail(function () { function ready_wrapper() {
loading_gadget_deferred.reject.apply(loading_gadget_deferred, return root_gadget;
arguments); }
queue.push(ready_wrapper);
for (i = 0; i < tmp_constructor.ready_list.length; i += 1) {
// Put a timeout?
queue.push(tmp_constructor.ready_list[i]);
// Always return the gadget instance after ready function
queue.push(ready_wrapper);
}
queue.push(resolve, function (e) {
reject(e);
console.warn(e);
throw e;
});
return queue;
}).fail(function (e) {
reject(e);
}); });
}); }
document.addEventListener('DOMContentLoaded', init, false);
}); });
} }
bootstrap(); bootstrap();
}(document, window, jQuery, DOMParser, Channel)); }(document, window, RSVP, DOMParser, Channel));
...@@ -6,10 +6,10 @@ ...@@ -6,10 +6,10 @@
<meta name="viewport" content="width=device-width, height=device-height"/> <meta name="viewport" content="width=device-width, height=device-height"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" href="../lib/qunit/qunit.css" type="text/css" media="screen"/> <link rel="stylesheet" href="../lib/qunit/qunit.css" type="text/css" media="screen"/>
<script src="../lib/rsvp/rsvp.js" type="text/javascript"></script>
<script src="../lib/jquery/jquery.js" type="text/javascript"></script> <script src="../lib/jquery/jquery.js" type="text/javascript"></script>
<script src="../lib/qunit/qunit.js" type="text/javascript"></script> <script src="../lib/qunit/qunit.js" type="text/javascript"></script>
<script src="../lib/sinon/sinon.js" type="text/javascript"></script> <script src="../lib/sinon/sinon.js" type="text/javascript"></script>
<script src="../sinon-qunit.js" type="text/javascript"></script>
<script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script> <script src="../../lib/jschannel/jschannel.js" type="text/javascript"></script>
<script src="../renderjs.js" type="text/javascript"></script> <script src="../renderjs.js" type="text/javascript"></script>
<script src="renderjs_test.js" type="text/javascript"></script> <script src="renderjs_test.js" type="text/javascript"></script>
......
/*global window, document, QUnit, jQuery, renderJS, RenderJSGadget, sinon */
/*jslint indent: 2, maxerr: 3, maxlen: 79 */ /*jslint indent: 2, maxerr: 3, maxlen: 79 */
/*global window, document, QUnit, jQuery, renderJS, RenderJSGadget, sinon,
RSVP, DOMParser */
/*jslint unparam: true, maxlen: 150 */
"use strict"; "use strict";
(function (document, $, renderJS, QUnit, sinon) { (function (document, $, renderJS, QUnit, sinon) {
...@@ -8,63 +10,81 @@ ...@@ -8,63 +10,81 @@
start = QUnit.start, start = QUnit.start,
ok = QUnit.ok, ok = QUnit.ok,
equal = QUnit.equal, equal = QUnit.equal,
expect = QUnit.expect,
throws = QUnit.throws, throws = QUnit.throws,
deepEqual = QUnit.deepEqual; deepEqual = QUnit.deepEqual,
root_gadget_klass = renderJS(window),
root_gadget_defer = RSVP.defer();
// Keep track of the root gadget
renderJS(window).ready(function (g) {
root_gadget_defer.resolve(g);
});
QUnit.config.testTimeout = 500;
// sinon.log = function (message) {
// console.log(message);
// };
function parseGadgetHTML(html) {
return renderJS.parseGadgetHTMLDocument(
(new DOMParser()).parseFromString(html, "text/html")
);
}
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// parseGadgetHTML // parseGadgetHTMLDocument
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
module("renderJS.parseGadgetHTML", { module("renderJS.parseGadgetHTMLDocument", {
setup: function () { setup: function () {
renderJS.clearGadgetKlassList(); renderJS.clearGadgetKlassList();
} }
}); });
test('Not valid HTML string', function () { test('Not valid HTML string', function () {
// Check that parseGadgetHTML returns the default value if the string is // Check that parseGadgetHTMLDocument returns the default value if the string is
// not a valid xml // not a valid xml
deepEqual(renderJS.parseGadgetHTML(""), { deepEqual(parseGadgetHTML(""), {
title: "", title: "",
interface_list: [], interface_list: [],
required_css_list: [], required_css_list: [],
required_js_list: [], required_js_list: [],
html: "",
}); });
}); });
test('Not string', function () { test('Not HTML Document', function () {
// Check that parseGadgetHTML throws an error if the parameter is not a // Check that parseGadgetHTMLDocument throws an error if the parameter is
// string // not a HTMLDocument
throws(function () { throws(function () {
renderJS.parseGadgetHTML({}); renderJS.parseGadgetHTMLDocument({});
}); });
}); });
test('Default result value', function () { test('Default result value', function () {
// Check default value returned by parseGadgetHTML // Check default value returned by parseGadgetHTMLDocument
deepEqual(renderJS.parseGadgetHTML(""), { deepEqual(renderJS.parseGadgetHTMLDocument(
document.implementation.createHTMLDocument("")
), {
title: "", title: "",
interface_list: [], interface_list: [],
required_css_list: [], required_css_list: [],
required_js_list: [], required_js_list: [],
html: "",
}); });
}); });
test('Extract title', function () { test('Extract title', function () {
// Check that parseGadgetHTML correctly extract the title // Check that parseGadgetHTMLDocument correctly extract the title
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
"<title>Great title</title>" + "<title>Great title</title>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
equal(settings.title, 'Great title', 'Title extracted'); equal(settings.title, 'Great title', 'Title extracted');
}); });
test('Extract only one title', function () { test('Extract only one title', function () {
// Check that parseGadgetHTML correctly extract the first title // Check that parseGadgetHTMLDocument correctly extract the first title
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -72,61 +92,62 @@ ...@@ -72,61 +92,62 @@
"<title>Great title 2</title>" + "<title>Great title 2</title>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
equal(settings.title, 'Great title', 'First title extracted'); equal(settings.title, 'Great title', 'First title extracted');
}); });
test('Extract title only from head', function () { // test('Extract title only from head', function () {
// Check that parseGadgetHTML only extract title from head // // Check that parseGadgetHTML only extract title from head
var settings, // var settings,
html = "<html>" + // html = "<html>" +
"<body>" + // "<body>" +
"<title>Great title</title>" + // "<title>Great title</title>" +
"</body></html>"; // "</body></html>";
//
settings = renderJS.parseGadgetHTML(html); // settings = renderJS.parseGadgetHTML(html);
equal(settings.title, '', 'Title not found'); // equal(settings.title, '', 'Title not found');
}); // });
test('Extract body', function () {
// Check that parseGadgetHTML correctly extract the body
var settings,
html = "<html>" +
"<body>" +
"<p>Foo</p>" +
"</body></html>";
settings = renderJS.parseGadgetHTML(html);
equal(settings.html, "<p>Foo</p>", "HTML extracted");
});
test('Extract all body', function () {
// Check that parseGadgetHTML correctly extracts all bodies
var settings,
html = "<html>" +
"<body>" +
"<p>Foo</p>" +
"</body><body>" +
"<p>Bar</p>" +
"</body></html>";
settings = renderJS.parseGadgetHTML(html);
equal(settings.html, '<p>Foo</p><p>Bar</p>', 'All bodies extracted');
});
test('Extract body only from html', function () {
// Check that parseGadgetHTML also extract body from head
var settings,
html = "<html>" +
"<head><body><p>Bar</p></body></head>" +
"</html>";
settings = renderJS.parseGadgetHTML(html); // XXX innerHTML is not extracted anymore
equal(settings.html, "<p>Bar</p>", "Body not found"); // test('Extract body', function () {
}); // // Check that parseGadgetHTML correctly extract the body
// var settings,
// html = "<html>" +
// "<body>" +
// "<p>Foo</p>" +
// "</body></html>";
//
// settings = renderJS.parseGadgetHTML(html);
// equal(settings.html, "<p>Foo</p>", "HTML extracted");
// });
//
// test('Extract all body', function () {
// // Check that parseGadgetHTML correctly extracts all bodies
// var settings,
// html = "<html>" +
// "<body>" +
// "<p>Foo</p>" +
// "</body><body>" +
// "<p>Bar</p>" +
// "</body></html>";
//
// settings = renderJS.parseGadgetHTML(html);
// equal(settings.html, '<p>Foo</p><p>Bar</p>', 'All bodies extracted');
// });
//
// test('Extract body only from html', function () {
// // Check that parseGadgetHTML also extract body from head
// var settings,
// html = "<html>" +
// "<head><body><p>Bar</p></body></head>" +
// "</html>";
//
// settings = renderJS.parseGadgetHTML(html);
// equal(settings.html, "<p>Bar</p>", "Body not found");
// });
test('Extract CSS', function () { test('Extract CSS', function () {
// Check that parseGadgetHTML correctly extract the CSS // Check that parseGadgetHTMLDocument correctly extract the CSS
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -134,14 +155,14 @@ ...@@ -134,14 +155,14 @@
"type='text/css'/>" + "type='text/css'/>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_css_list, deepEqual(settings.required_css_list,
['../lib/qunit/qunit.css'], ['../lib/qunit/qunit.css'],
"CSS extracted"); "CSS extracted");
}); });
test('Extract CSS order', function () { test('Extract CSS order', function () {
// Check that parseGadgetHTML correctly keep CSS order // Check that parseGadgetHTMLDocument correctly keep CSS order
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -151,14 +172,14 @@ ...@@ -151,14 +172,14 @@
"type='text/css'/>" + "type='text/css'/>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_css_list, deepEqual(settings.required_css_list,
['../lib/qunit/qunit.css', '../lib/qunit/qunit2.css'], ['../lib/qunit/qunit.css', '../lib/qunit/qunit2.css'],
"CSS order kept"); "CSS order kept");
}); });
test('Extract CSS only from head', function () { test('Extract CSS only from head', function () {
// Check that parseGadgetHTML only extract css from head // Check that parseGadgetHTMLDocument only extract css from head
var settings, var settings,
html = "<html>" + html = "<html>" +
"<body>" + "<body>" +
...@@ -166,12 +187,12 @@ ...@@ -166,12 +187,12 @@
"type='text/css'/>" + "type='text/css'/>" +
"</body></html>"; "</body></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_css_list, [], "CSS not found"); deepEqual(settings.required_css_list, [], "CSS not found");
}); });
test('Extract interface', function () { test('Extract interface', function () {
// Check that parseGadgetHTML correctly extract the interface // Check that parseGadgetHTMLDocument correctly extract the interface
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -179,14 +200,14 @@ ...@@ -179,14 +200,14 @@
" href='./interface/renderable'/>" + " href='./interface/renderable'/>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.interface_list, deepEqual(settings.interface_list,
['./interface/renderable'], ['./interface/renderable'],
"interface extracted"); "interface extracted");
}); });
test('Extract interface order', function () { test('Extract interface order', function () {
// Check that parseGadgetHTML correctly keep interface order // Check that parseGadgetHTMLDocument correctly keep interface order
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -196,7 +217,7 @@ ...@@ -196,7 +217,7 @@
" href='./interface/field'/>" + " href='./interface/field'/>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.interface_list, deepEqual(settings.interface_list,
['./interface/renderable', ['./interface/renderable',
'./interface/field'], './interface/field'],
...@@ -204,7 +225,7 @@ ...@@ -204,7 +225,7 @@
}); });
test('Extract interface only from head', function () { test('Extract interface only from head', function () {
// Check that parseGadgetHTML only extract interface from head // Check that parseGadgetHTMLDocument only extract interface from head
var settings, var settings,
html = "<html>" + html = "<html>" +
"<body>" + "<body>" +
...@@ -212,12 +233,12 @@ ...@@ -212,12 +233,12 @@
" href='./interface/renderable'/>" + " href='./interface/renderable'/>" +
"</body></html>"; "</body></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.interface_list, [], "interface not found"); deepEqual(settings.interface_list, [], "interface not found");
}); });
test('Extract JS', function () { test('Extract JS', function () {
// Check that parseGadgetHTML correctly extract the JS // Check that parseGadgetHTMLDocument correctly extract the JS
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -225,14 +246,14 @@ ...@@ -225,14 +246,14 @@
"type='text/javascript'></script>" + "type='text/javascript'></script>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_js_list, deepEqual(settings.required_js_list,
['../lib/qunit/qunit.js'], ['../lib/qunit/qunit.js'],
"JS extracted"); "JS extracted");
}); });
test('Extract JS order', function () { test('Extract JS order', function () {
// Check that parseGadgetHTML correctly keep JS order // Check that parseGadgetHTMLDocument correctly keep JS order
var settings, var settings,
html = "<html>" + html = "<html>" +
"<head>" + "<head>" +
...@@ -242,14 +263,14 @@ ...@@ -242,14 +263,14 @@
"type='text/javascript'></script>" + "type='text/javascript'></script>" +
"</head></html>"; "</head></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_js_list, deepEqual(settings.required_js_list,
['../lib/qunit/qunit.js', '../lib/qunit/qunit2.js'], ['../lib/qunit/qunit.js', '../lib/qunit/qunit2.js'],
"JS order kept"); "JS order kept");
}); });
test('Extract JS only from head', function () { test('Extract JS only from head', function () {
// Check that parseGadgetHTML only extract js from head // Check that parseGadgetHTMLDocument only extract js from head
var settings, var settings,
html = "<html>" + html = "<html>" +
"<body>" + "<body>" +
...@@ -257,13 +278,13 @@ ...@@ -257,13 +278,13 @@
"type='text/javascript'></script>" + "type='text/javascript'></script>" +
"</body></html>"; "</body></html>";
settings = renderJS.parseGadgetHTML(html); settings = parseGadgetHTML(html);
deepEqual(settings.required_js_list, [], "JS not found"); deepEqual(settings.required_js_list, [], "JS not found");
}); });
test('Non valid XML (HTML in fact...)', function () { test('Non valid XML (HTML in fact...)', function () {
// Check default value returned by parseGadgetHTML // Check default value returned by parseGadgetHTMLDocument
deepEqual(renderJS.parseGadgetHTML('<!doctype html><html><head>' + deepEqual(parseGadgetHTML('<!doctype html><html><head>' +
'<title>Test non valid XML</title>' + '<title>Test non valid XML</title>' +
'<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' + '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">' +
'</head><body><p>Non valid XML</p></body></html>'), { '</head><body><p>Non valid XML</p></body></html>'), {
...@@ -271,7 +292,7 @@ ...@@ -271,7 +292,7 @@
interface_list: [], interface_list: [],
required_css_list: [], required_css_list: [],
required_js_list: [], required_js_list: [],
html: "<p>Non valid XML</p>", // html: "<p>Non valid XML</p>",
}); });
}); });
...@@ -288,22 +309,26 @@ ...@@ -288,22 +309,26 @@
var server = sinon.fakeServer.create(), var server = sinon.fakeServer.create(),
url = 'https://example.org/files/qunittest/test'; url = 'https://example.org/files/qunittest/test';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [404, { server.respondWith("GET", url, [404, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "foo"]); }, "foo"]);
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function () { .then(function () {
ok(false, "404 should fail"); ok(false, "404 should fail");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (xhr) {
equal("404", jqXHR.status); equal(xhr.status, 404);
equal(xhr.url, url);
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
test('Non HTML reject the promise', function () { test('Non HTML reject the promise', function () {
...@@ -311,24 +336,26 @@ ...@@ -311,24 +336,26 @@
var server = sinon.fakeServer.create(), var server = sinon.fakeServer.create(),
url = 'https://example.org/files/qunittest/test'; url = 'https://example.org/files/qunittest/test';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/plain", "Content-Type": "text/plain",
}, "foo"]); }, "foo"]);
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function () { .then(function () {
ok(false, "text/plain should fail"); ok(false, "text/plain should fail");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
equal(jqXHR.status, "200"); equal(jqXHR.status, 200);
equal(textStatus, "Unexpected content type");
equal(jqXHR.getResponseHeader("Content-Type"), "text/plain"); equal(jqXHR.getResponseHeader("Content-Type"), "text/plain");
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
test('HTML parsing failure reject the promise', function () { test('HTML parsing failure reject the promise', function () {
...@@ -337,29 +364,32 @@ ...@@ -337,29 +364,32 @@
url = 'https://example.org/files/qunittest/test', url = 'https://example.org/files/qunittest/test',
mock; mock;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, ""]); }, ""]);
mock = this.mock(renderJS, "parseGadgetHTML", function () { mock = sinon.mock(renderJS, "parseGadgetHTMLDocument", function () {
throw new Error(); throw new Error("foo");
}); });
mock.expects("parseGadgetHTML").once().throws(); mock.expects("parseGadgetHTMLDocument").once().throws();
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function () { .then(function () {
ok(false, "Non parsable HTML should fail"); ok(false, "Non parsable HTML should fail");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
equal("200", jqXHR.status); ok(e instanceof Error);
equal(textStatus, "HTML Parsing failed");
}) })
.always(function () { .always(function () {
mock.verify();
start(); start();
mock.verify();
mock.restore();
server.restore();
}); });
server.respond();
}); });
test('Klass creation', function () { test('Klass creation', function () {
...@@ -369,36 +399,41 @@ ...@@ -369,36 +399,41 @@
url = 'https://example.org/files/qunittest/test', url = 'https://example.org/files/qunittest/test',
mock; mock;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "foo"]); }, "foo"]);
mock = this.mock(renderJS, "parseGadgetHTML"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTML").once().withArgs("foo").returns( mock.expects("parseGadgetHTMLDocument").once().returns(
{foo: 'bar'} {foo: 'bar'}
); );
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function (Klass) { .then(function (Klass) {
var instance; var instance;
equal(Klass.prototype.path, url); equal(Klass.prototype.path, url);
equal(Klass.prototype.foo, 'bar'); equal(Klass.prototype.foo, 'bar');
equal(Klass.template_element.nodeType, 9);
instance = new Klass(); instance = new Klass();
ok(instance instanceof RenderJSGadget); ok(instance instanceof RenderJSGadget);
ok(instance instanceof Klass); ok(instance instanceof Klass);
ok(Klass !== RenderJSGadget); ok(Klass !== RenderJSGadget);
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, JSON.stringify(e));
}) })
.always(function () { .always(function () {
mock.verify();
start(); start();
mock.verify();
mock.restore();
server.restore();
}); });
server.respond();
}); });
test('Klass is not reloaded if called twice', function () { test('Klass is not reloaded if called twice', function () {
...@@ -406,38 +441,39 @@ ...@@ -406,38 +441,39 @@
// if it has already been loaded // if it has already been loaded
var server = sinon.fakeServer.create(), var server = sinon.fakeServer.create(),
url = 'https://example.org/files/qunittest/test', url = 'https://example.org/files/qunittest/test',
klass1,
mock; mock;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "foo"]); }, "foo"]);
mock = this.mock(renderJS, "parseGadgetHTML"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTML").once().withArgs("foo").returns( mock.expects("parseGadgetHTMLDocument").once().returns(
{foo: 'bar'} {foo: 'bar'}
); );
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function (Klass1) { .then(function (Klass1) {
renderJS.declareGadgetKlass(url) klass1 = Klass1;
.done(function (Klass2) { return renderJS.declareGadgetKlass(url);
equal(Klass1, Klass2); })
}) .then(function (Klass2) {
.fail(function (jqXHR, textStatus) { equal(klass1, Klass2);
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); })
}) .fail(function (jqXHR) {
.always(function () { ok(false, "Failed to load " + jqXHR.status);
start();
mock.verify();
});
}) })
.fail(function (jqXHR, textStatus) { .always(function () {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
start(); start();
mock.verify();
mock.restore();
server.restore();
}); });
server.respond();
}); });
test('Content type parameter are supported', function () { test('Content type parameter are supported', function () {
...@@ -446,13 +482,16 @@ ...@@ -446,13 +482,16 @@
var server = sinon.fakeServer.create(), var server = sinon.fakeServer.create(),
url = 'https://example.org/files/qunittest/test'; url = 'https://example.org/files/qunittest/test';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html; charset=utf-8", "Content-Type": "text/html; charset=utf-8",
}, "<html></html>"]); }, "<html></html>"]);
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function (Klass) { .then(function (Klass) {
var instance; var instance;
equal(Klass.prototype.path, url); equal(Klass.prototype.path, url);
...@@ -462,13 +501,13 @@ ...@@ -462,13 +501,13 @@
ok(instance instanceof Klass); ok(instance instanceof Klass);
ok(Klass !== RenderJSGadget); ok(Klass !== RenderJSGadget);
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, "Failed to load " + jqXHR.status);
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -481,15 +520,16 @@ ...@@ -481,15 +520,16 @@
}); });
test('Download error reject the promise', function () { test('Download error reject the promise', function () {
// Check that declareJS fails if ajax fails // Check that declareJS fails if ajax fails
var url = 'foo://bar'; var url = 'http://0.0.0.0/bar';
stop(); stop();
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function () { .then(function () {
ok(false, "404 should fail"); ok(false, "404 should fail");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
equal(jqXHR.status, "404"); equal(e.type, "error");
equal(e.target.getAttribute("src"), url);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -498,21 +538,22 @@ ...@@ -498,21 +538,22 @@
test('Ajax error reject the promise twice', function () { test('Ajax error reject the promise twice', function () {
// Check that failed declareJS is not cached // Check that failed declareJS is not cached
var url = 'foo://bar'; var url = 'http://0.0.0.0/bar2';
stop(); stop();
renderJS.declareJS(url) renderJS.declareJS(url)
.always(function () { .always(function () {
renderJS.declareJS(url) return renderJS.declareJS(url);
.done(function () { })
ok(false, "404 should fail"); .then(function () {
}) ok(false, "404 should fail");
.fail(function (jqXHR, textStatus) { })
equal(jqXHR.status, "404"); .fail(function (e) {
}) equal(e.type, "error");
.always(function () { equal(e.target.getAttribute("src"), url);
start(); })
}); .always(function () {
start();
}); });
}); });
...@@ -525,11 +566,11 @@ ...@@ -525,11 +566,11 @@
stop(); stop();
window.onerror = undefined; window.onerror = undefined;
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function (value, textStatus, jqXHR) { .then(function (value, textStatus, jqXHR) {
ok(ok, "Non JS mime type should load"); ok(ok, "Non JS mime type should load");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, jqXHR);
}) })
.always(function () { .always(function () {
window.onerror = previousonerror; window.onerror = previousonerror;
...@@ -544,11 +585,11 @@ ...@@ -544,11 +585,11 @@
stop(); stop();
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function () { .then(function () {
equal($("#qunit-fixture").text(), "JS fetched and loaded"); equal($("#qunit-fixture").text(), "JS fetched and loaded");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, "Failed to load " + jqXHR);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -564,11 +605,11 @@ ...@@ -564,11 +605,11 @@
stop(); stop();
window.onerror = undefined; window.onerror = undefined;
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function (aaa) { .then(function (aaa) {
ok(true, "JS with error cleanly loaded"); ok(true, "JS with error cleanly loaded");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, jqXHR);
}) })
.always(function () { .always(function () {
window.onerror = previousonerror; window.onerror = previousonerror;
...@@ -583,22 +624,18 @@ ...@@ -583,22 +624,18 @@
stop(); stop();
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function () { .then(function () {
equal($("#qunit-fixture").text(), "JS not fetched twice"); equal($("#qunit-fixture").text(), "JS not fetched twice");
$("#qunit-fixture").text(""); $("#qunit-fixture").text("");
renderJS.declareJS(url) return renderJS.declareJS(url);
.done(function () { })
equal($("#qunit-fixture").text(), ""); .then(function () {
}) equal($("#qunit-fixture").text(), "");
.fail(function (jqXHR, textStatus) { })
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); .fail(function (jqXHR) {
}) ok(false, "Failed to load " + jqXHR);
.always(function () {
start();
});
}) })
.fail(function (jqXHR, textStatus) { .always(function () {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
start(); start();
}); });
}); });
...@@ -614,34 +651,36 @@ ...@@ -614,34 +651,36 @@
test('Ajax error resolve the promise', function () { test('Ajax error resolve the promise', function () {
// Check that declareCSS is resolved if ajax fails // Check that declareCSS is resolved if ajax fails
var url = 'foo://bar'; var url = 'foo//://bar';
expect(1);
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function () { .then(function () {
ok(true, "404 should fail"); ok(false, "404 should fail");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
ok(false); equal(e.type, "error");
equal(e.target.getAttribute("href"), url);
}) })
.always(function () { .always(function () {
start(); start();
}); });
}); });
test('Non CSS resolve the promise', function () { test('Non CSS reject the promise', function () {
// Check that declareCSS is resolved if mime type is wrong // Check that declareCSS is resolved if mime type is wrong
var url = "data:image/png;base64," + var url = "data:image/png;base64," +
window.btoa("= = ="); window.btoa("= = =");
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function (value, textStatus, jqXHR) { .then(function (value, textStatus, jqXHR) {
// Chrome accept the css
ok(true, "Non CSS mime type should load"); ok(true, "Non CSS mime type should load");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
ok(false); equal(e.type, "error");
equal(e.target.getAttribute("href"), url);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -655,7 +694,7 @@ ...@@ -655,7 +694,7 @@
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function () { .then(function () {
var found = false; var found = false;
$('head').find('link[rel=stylesheet]').each(function (i, style) { $('head').find('link[rel=stylesheet]').each(function (i, style) {
if (style.href === url) { if (style.href === url) {
...@@ -665,8 +704,8 @@ ...@@ -665,8 +704,8 @@
ok(found, "CSS in the head"); ok(found, "CSS in the head");
equal($("#qunit-fixture").css("background-color"), "rgb(255, 0, 0)"); equal($("#qunit-fixture").css("background-color"), "rgb(255, 0, 0)");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, e);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -681,11 +720,12 @@ ...@@ -681,11 +720,12 @@
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function () { .then(function () {
// Chrome does not consider this as error
ok(true, "CSS with error cleanly loaded"); ok(true, "CSS with error cleanly loaded");
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(true, jqXHR);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -699,7 +739,7 @@ ...@@ -699,7 +739,7 @@
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function () { .then(function () {
equal($("#qunit-fixture").css("background-color"), "rgb(0, 0, 255)"); equal($("#qunit-fixture").css("background-color"), "rgb(0, 0, 255)");
$('head').find('link[rel=stylesheet]').each(function (i, style) { $('head').find('link[rel=stylesheet]').each(function (i, style) {
if (style.href === url) { if (style.href === url) {
...@@ -708,27 +748,23 @@ ...@@ -708,27 +748,23 @@
}); });
ok($("#qunit-fixture").css("background-color") !== "rgb(0, 0, 255)"); ok($("#qunit-fixture").css("background-color") !== "rgb(0, 0, 255)");
renderJS.declareCSS(url) return renderJS.declareCSS(url);
.done(function () {
var found = false;
$('head').find('link[rel=stylesheet]').each(function (i, style) {
if (style.href === url) {
found = true;
}
});
ok($("#qunit-fixture").css("background-color") !==
"rgb(0, 0, 255)", $("#qunit-fixture").css("background-color"));
ok(!found);
})
.fail(function (jqXHR, textStatus) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
})
.always(function () {
start();
});
}) })
.fail(function (jqXHR, textStatus) { .then(function () {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); var found = false;
$('head').find('link[rel=stylesheet]').each(function (i, style) {
if (style.href === url) {
found = true;
}
});
ok($("#qunit-fixture").css("background-color") !==
"rgb(0, 0, 255)", $("#qunit-fixture").css("background-color"));
ok(!found);
})
.fail(function (jqXHR) {
ok(false, "Failed to load " + jqXHR);
})
.always(function () {
start(); start();
}); });
}); });
...@@ -747,41 +783,42 @@ ...@@ -747,41 +783,42 @@
// after clearGadgetKlassList is called // after clearGadgetKlassList is called
var server = sinon.fakeServer.create(), var server = sinon.fakeServer.create(),
url = 'https://example.org/files/qunittest/test', url = 'https://example.org/files/qunittest/test',
klass1,
mock; mock;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "foo"]); }, "foo"]);
mock = this.mock(renderJS, "parseGadgetHTML"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTML").twice().withArgs("foo").returns( mock.expects("parseGadgetHTMLDocument").twice().returns(
{foo: 'bar'} {foo: 'bar'}
); );
stop(); stop();
renderJS.declareGadgetKlass(url) renderJS.declareGadgetKlass(url)
.done(function (Klass1) { .then(function (Klass1) {
klass1 = Klass1;
renderJS.clearGadgetKlassList(); renderJS.clearGadgetKlassList();
renderJS.declareGadgetKlass(url) return renderJS.declareGadgetKlass(url);
.done(function (Klass2) {
mock.verify();
ok(Klass1 !== Klass2);
})
.fail(function (jqXHR, textStatus) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
})
.always(function () {
start();
});
}) })
.fail(function (jqXHR, textStatus) { .then(function (Klass2) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(klass1 !== Klass2);
})
.fail(function (jqXHR) {
ok(false, jqXHR);
})
.always(function () {
start(); start();
server.restore();
mock.verify();
mock.restore();
}); });
server.respond();
}); });
test('clearGadgetKlassList leads to JS reload', function () { test('clearGadgetKlassList leads to JS reload', function () {
...@@ -792,23 +829,19 @@ ...@@ -792,23 +829,19 @@
stop(); stop();
renderJS.declareJS(url) renderJS.declareJS(url)
.done(function () { .then(function () {
renderJS.clearGadgetKlassList(); renderJS.clearGadgetKlassList();
equal($("#qunit-fixture").text(), "JS not fetched twice"); equal($("#qunit-fixture").text(), "JS not fetched twice");
$("#qunit-fixture").text(""); $("#qunit-fixture").text("");
renderJS.declareJS(url) return renderJS.declareJS(url);
.done(function () { })
equal($("#qunit-fixture").text(), "JS not fetched twice"); .then(function () {
}) equal($("#qunit-fixture").text(), "JS not fetched twice");
.fail(function (jqXHR, textStatus) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
})
.always(function () {
start();
});
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); ok(false, "Failed to load " + jqXHR);
})
.always(function () {
start(); start();
}); });
}); });
...@@ -822,26 +855,60 @@ ...@@ -822,26 +855,60 @@
stop(); stop();
renderJS.declareCSS(url) renderJS.declareCSS(url)
.done(function () { .then(function () {
renderJS.clearGadgetKlassList(); renderJS.clearGadgetKlassList();
equal($('head').find('link[rel=stylesheet]').length, count + 1); equal($('head').find('link[rel=stylesheet]').length, count + 1);
renderJS.declareCSS(url) return renderJS.declareCSS(url);
.done(function () {
equal($('head').find('link[rel=stylesheet]').length, count + 2);
})
.fail(function (jqXHR, textStatus) {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status);
})
.always(function () {
start();
});
}) })
.fail(function (jqXHR, textStatus) { .then(function () {
ok(false, "Failed to load " + textStatus + " " + jqXHR.status); equal($('head').find('link[rel=stylesheet]').length, count + 2);
})
.fail(function (jqXHR) {
ok(false, "Failed to load " + jqXHR);
})
.always(function () {
start(); start();
}); });
}); });
/////////////////////////////////////////////////////////////////
// RenderJSGadget
/////////////////////////////////////////////////////////////////
module("RenderJSGadget");
test('should be a constructor', function () {
var gadget = new RenderJSGadget();
equal(
Object.getPrototypeOf(gadget),
RenderJSGadget.prototype,
'[[Prototype]] equals RenderJSGadget.prototype'
);
equal(
gadget.constructor,
RenderJSGadget,
'constructor property of instances is set correctly'
);
equal(
RenderJSGadget.prototype.constructor,
RenderJSGadget,
'constructor property of prototype is set correctly'
);
});
test('should not accept parameter', function () {
equal(RenderJSGadget.length, 0);
});
test('should work without new', function () {
var gadgetKlass = RenderJSGadget,
gadget = gadgetKlass();
equal(
gadget.constructor,
RenderJSGadget,
'constructor property of instances is set correctly'
);
});
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// RenderJSGadget.getInterfaceList // RenderJSGadget.getInterfaceList
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
...@@ -856,7 +923,7 @@ ...@@ -856,7 +923,7 @@
gadget.interface_list = "foo"; gadget.interface_list = "foo";
stop(); stop();
gadget.getInterfaceList() gadget.getInterfaceList()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -869,7 +936,7 @@ ...@@ -869,7 +936,7 @@
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getInterfaceList() gadget.getInterfaceList()
.done(function (result) { .then(function (result) {
deepEqual(result, []); deepEqual(result, []);
}) })
.always(function () { .always(function () {
...@@ -891,7 +958,7 @@ ...@@ -891,7 +958,7 @@
gadget.required_css_list = "foo"; gadget.required_css_list = "foo";
stop(); stop();
gadget.getRequiredCSSList() gadget.getRequiredCSSList()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -904,7 +971,7 @@ ...@@ -904,7 +971,7 @@
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getRequiredCSSList() gadget.getRequiredCSSList()
.done(function (result) { .then(function (result) {
deepEqual(result, []); deepEqual(result, []);
}) })
.always(function () { .always(function () {
...@@ -926,7 +993,7 @@ ...@@ -926,7 +993,7 @@
gadget.required_js_list = "foo"; gadget.required_js_list = "foo";
stop(); stop();
gadget.getRequiredJSList() gadget.getRequiredJSList()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -939,7 +1006,7 @@ ...@@ -939,7 +1006,7 @@
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getRequiredJSList() gadget.getRequiredJSList()
.done(function (result) { .then(function (result) {
deepEqual(result, []); deepEqual(result, []);
}) })
.always(function () { .always(function () {
...@@ -961,7 +1028,7 @@ ...@@ -961,7 +1028,7 @@
gadget.path = "foo"; gadget.path = "foo";
stop(); stop();
gadget.getPath() gadget.getPath()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -974,7 +1041,7 @@ ...@@ -974,7 +1041,7 @@
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getPath() gadget.getPath()
.done(function (result) { .then(function (result) {
equal(result, ""); equal(result, "");
}) })
.always(function () { .always(function () {
...@@ -996,7 +1063,7 @@ ...@@ -996,7 +1063,7 @@
gadget.title = "foo"; gadget.title = "foo";
stop(); stop();
gadget.getTitle() gadget.getTitle()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -1009,7 +1076,7 @@ ...@@ -1009,7 +1076,7 @@
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getTitle() gadget.getTitle()
.done(function (result) { .then(function (result) {
equal(result, ""); equal(result, "");
}) })
.always(function () { .always(function () {
...@@ -1018,20 +1085,20 @@ ...@@ -1018,20 +1085,20 @@
}); });
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
// RenderJSGadget.getHTML // RenderJSGadget.getElement
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
module("RenderJSGadget.getHTML", { module("RenderJSGadget.getElement", {
setup: function () { setup: function () {
renderJS.clearGadgetKlassList(); renderJS.clearGadgetKlassList();
} }
}); });
test('returns html', function () { test('returns element property', function () {
// Check that getHTML return a Promise // Check that getElement return a Promise
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
gadget.html = "foo"; gadget.element = "foo";
stop(); stop();
gadget.getHTML() gadget.getElement()
.done(function (result) { .then(function (result) {
equal(result, "foo"); equal(result, "foo");
}) })
.always(function () { .always(function () {
...@@ -1039,13 +1106,17 @@ ...@@ -1039,13 +1106,17 @@
}); });
}); });
test('default value', function () { test('throw an error if no element is defined', function () {
// Check that getHTML return a Promise // Check that getElement return a Promise
var gadget = new RenderJSGadget(); var gadget = new RenderJSGadget();
stop(); stop();
gadget.getHTML() gadget.getElement()
.done(function (result) { .then(function () {
equal(result, ""); ok(false, "getElement should fail");
})
.fail(function (e) {
console.log(e);
ok(e instanceof Error);
}) })
.always(function () { .always(function () {
start(); start();
...@@ -1074,7 +1145,7 @@ ...@@ -1074,7 +1145,7 @@
gadget = new Klass(); gadget = new Klass();
equal(gadget.testFoo, undefined); equal(gadget.testFoo, undefined);
result = Klass.declareMethod('testFoo', function () { result = Klass.declareMethod('testFoo', function () {
var a; console.log("");
}); });
// declareMethod is chainable // declareMethod is chainable
equal(result, Klass); equal(result, Klass);
...@@ -1086,7 +1157,7 @@ ...@@ -1086,7 +1157,7 @@
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var Klass = function () { var Klass = function () {
RenderJSGadget.call(this); RenderJSGadget.call(this);
}, gadget, called, result; }, gadget, called;
Klass.prototype = new RenderJSGadget(); Klass.prototype = new RenderJSGadget();
Klass.prototype.constructor = Klass; Klass.prototype.constructor = Klass;
Klass.declareMethod = RenderJSGadget.declareMethod; Klass.declareMethod = RenderJSGadget.declareMethod;
...@@ -1102,9 +1173,18 @@ ...@@ -1102,9 +1173,18 @@
ok(Klass.prototype.testFoo !== undefined); ok(Klass.prototype.testFoo !== undefined);
equal(Klass.prototype.testFoo, gadget.testFoo); equal(Klass.prototype.testFoo, gadget.testFoo);
stop();
// method can be called // method can be called
gadget.testFoo("Bar"); gadget.testFoo("Bar")
equal(called, "Bar"); .then(function (param) {
equal(called, "Bar");
})
.fail(function () {
ok(false, "Should propagate the parameters");
})
.always(function () {
start();
});
}); });
test('returns a promise when synchronous function', function () { test('returns a promise when synchronous function', function () {
...@@ -1127,7 +1207,7 @@ ...@@ -1127,7 +1207,7 @@
// method can be called // method can be called
stop(); stop();
gadget.testFoo("Bar") gadget.testFoo("Bar")
.done(function (param) { .then(function (param) {
equal(param, "Bar"); equal(param, "Bar");
}) })
.fail(function () { .fail(function () {
...@@ -1152,16 +1232,14 @@ ...@@ -1152,16 +1232,14 @@
gadget = new Klass(); gadget = new Klass();
Klass.declareMethod('testFoo', function (value) { Klass.declareMethod('testFoo', function (value) {
var dfr = $.Deferred(); var dfr = $.Deferred();
setTimeout(function () { dfr.reject(value);
dfr.reject(value);
});
return dfr.promise(); return dfr.promise();
}); });
// method can be called // method can be called
stop(); stop();
gadget.testFoo("Bar") gadget.testFoo("Bar")
.done(function () { .then(function () {
ok(false, "Callback promise is rejected"); ok(false, "Callback promise is rejected");
}) })
.fail(function (param) { .fail(function (param) {
...@@ -1186,15 +1264,14 @@ ...@@ -1186,15 +1264,14 @@
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var Klass = function () { var Klass = function () {
RenderJSGadget.call(this); RenderJSGadget.call(this);
}, gadget, result; }, result;
Klass.prototype = new RenderJSGadget(); Klass.prototype = new RenderJSGadget();
Klass.prototype.constructor = Klass; Klass.prototype.constructor = Klass;
Klass.ready_list = []; Klass.ready_list = [];
Klass.ready = RenderJSGadget.ready; Klass.ready = RenderJSGadget.ready;
gadget = new Klass();
result = Klass.ready(function () { result = Klass.ready(function () {
var a; console.log("");
}); });
// ready is chainable // ready is chainable
equal(result, Klass); equal(result, Klass);
...@@ -1206,14 +1283,13 @@ ...@@ -1206,14 +1283,13 @@
// Subclass RenderJSGadget to not pollute its namespace // Subclass RenderJSGadget to not pollute its namespace
var Klass = function () { var Klass = function () {
RenderJSGadget.call(this); RenderJSGadget.call(this);
}, gadget, result, },
callback = function () {var a; }; callback = function () {console.log(""); };
Klass.prototype = new RenderJSGadget(); Klass.prototype = new RenderJSGadget();
Klass.prototype.constructor = Klass; Klass.prototype.constructor = Klass;
Klass.ready_list = []; Klass.ready_list = [];
Klass.ready = RenderJSGadget.ready; Klass.ready = RenderJSGadget.ready;
gadget = new Klass();
Klass.ready(callback); Klass.ready(callback);
// ready is chainable // ready is chainable
deepEqual(Klass.ready_list, [callback]); deepEqual(Klass.ready_list, [callback]);
...@@ -1238,17 +1314,26 @@ ...@@ -1238,17 +1314,26 @@
"type='text/javascript'></script>" + "type='text/javascript'></script>" +
"</body></html>"; "</body></html>";
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, html]); }, html]);
stop(); stop();
gadget.declareGadget(url, $('#qunit-fixture')) gadget.declareGadget(url)//, $('#qunit-fixture'))
.always(function () { .then(function () {
ok(true); ok(true);
})
.fail(function (e) {
console.warn(e);
ok(false, e);
})
.always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
test('provide a gadget instance as callback parameter', function () { test('provide a gadget instance as callback parameter', function () {
...@@ -1262,19 +1347,22 @@ ...@@ -1262,19 +1347,22 @@
"type='text/javascript'></script>" + "type='text/javascript'></script>" +
"</body></html>"; "</body></html>";
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", url, [200, { server.respondWith("GET", url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, html]); }, html]);
stop(); stop();
gadget.declareGadget(url, $('#qunit-fixture')) gadget.declareGadget(url)//, $('#qunit-fixture'))
.done(function (new_gadget) { .then(function (new_gadget) {
equal(new_gadget.path, url); equal(new_gadget.path, url);
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
// test('no parameter', function () { // test('no parameter', function () {
...@@ -1303,7 +1391,7 @@ ...@@ -1303,7 +1391,7 @@
window.btoa( window.btoa(
"$('#qunit-fixture').find('div').first().text('youhou2');" "$('#qunit-fixture').find('div').first().text('youhou2');"
), ),
css1_url = "data:text/plain;base64," + css1_url = "data:text/css;base64," +
window.btoa(""), window.btoa(""),
css2_url = css1_url, css2_url = css1_url,
html = "<html>" + html = "<html>" +
...@@ -1314,30 +1402,34 @@ ...@@ -1314,30 +1402,34 @@
"<link rel='stylesheet' href='" + css1_url + "' type='text/css'/>" + "<link rel='stylesheet' href='" + css1_url + "' type='text/css'/>" +
"<link rel='stylesheet' href='" + css2_url + "' type='text/css'/>" + "<link rel='stylesheet' href='" + css2_url + "' type='text/css'/>" +
"</head><body><p>Bar content</p></body></html>", "</head><body><p>Bar content</p></body></html>",
mock,
spy_js, spy_js,
spy_css; spy_css;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, { server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, html]); }, html]);
spy_js = this.spy(renderJS, "declareJS"); spy_js = sinon.spy(renderJS, "declareJS");
spy_css = this.spy(renderJS, "declareCSS"); spy_css = sinon.spy(renderJS, "declareCSS");
mock = this.mock(renderJS, "parseGadgetHTML"); // mock = sinon.mock(renderJS, "parseGadgetHTML");
mock.expects("parseGadgetHTML").once().withArgs(html).returns({ // mock.expects("parseGadgetHTML").once().withArgs(html).returns({
required_js_list: [js1_url, js2_url], // required_js_list: [js1_url, js2_url],
required_css_list: [css1_url, css2_url], // required_css_list: [css1_url, css2_url],
html: "<p>Bar content</p>", // html: "<p>Bar content</p>",
}); // });
$('#qunit-fixture').html("<div></div><div></div>"); $('#qunit-fixture').html("<div></div><div>bar</div>");
stop(); stop();
gadget.declareGadget(html_url, $('#qunit-fixture').find("div").last()) gadget.declareGadget(html_url)//, $('#qunit-fixture').find("div").last()[0])
.done(function (new_gadget) { .then(function (new_gadget) {
equal($('#qunit-fixture').html(), equal($('#qunit-fixture').html(),
"<div>youhou2</div><div><p>Bar content</p></div>"); "<div>youhou2</div><div>bar</div>");
equal(new_gadget.element.outerHTML,
"<div><p>Bar content</p></div>");
ok(spy_js.calledTwice, "JS count " + spy_js.callCount); ok(spy_js.calledTwice, "JS count " + spy_js.callCount);
equal(spy_js.firstCall.args[0], js1_url, "First JS call"); equal(spy_js.firstCall.args[0], js1_url, "First JS call");
equal(spy_js.secondCall.args[0], js2_url, "Second JS call"); equal(spy_js.secondCall.args[0], js2_url, "Second JS call");
...@@ -1345,13 +1437,16 @@ ...@@ -1345,13 +1437,16 @@
equal(spy_css.firstCall.args[0], css1_url, "First CSS call"); equal(spy_css.firstCall.args[0], css1_url, "First CSS call");
equal(spy_css.secondCall.args[0], css2_url, "Second CSS call"); equal(spy_css.secondCall.args[0], css2_url, "Second CSS call");
}) })
.fail(function () { .fail(function (e) {
console.warn(e);
ok(false); ok(false);
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
spy_js.restore();
spy_css.restore();
}); });
server.respond();
}); });
// test('load dependency in the right order', function () { // test('load dependency in the right order', function () {
...@@ -1368,135 +1463,274 @@ ...@@ -1368,135 +1463,274 @@
server = sinon.fakeServer.create(), server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test3.html'; html_url = 'https://example.org/files/qunittest/test3.html';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [404, { server.respondWith("GET", html_url, [404, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, ""]); }, ""]);
stop(); stop();
gadget.declareGadget(html_url, $('#qunit-fixture').find("div").last()) gadget.declareGadget(html_url)//, $('#qunit-fixture').find("div").last())
.done(function (new_gadget) { .then(function (new_gadget) {
ok(false); ok(false);
}) })
.fail(function (jqXHR, textStatus) { .fail(function (jqXHR) {
equal("404", jqXHR.status); equal(jqXHR.status, 404);
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
}); });
server.respond();
}); });
test('Fail if js can not be loaded', function () { test('Fail if js can not be loaded', function () {
// Check that dependencies are loaded before gadget creation // Check that dependencies are loaded before gadget creation
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
server = sinon.fakeServer.create(), server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test2.html', html_url = 'http://example.org/files/qunittest/test5.html',
js1_url = 'foo://bar2', js1_url = 'http://0.0.0.0/test.js',
mock; mock;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, { server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "raw html"]); }, "raw html"]);
mock = this.mock(renderJS, "parseGadgetHTML"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTML").once().withArgs("raw html").returns({ mock.expects("parseGadgetHTMLDocument").once().returns({
required_js_list: [js1_url] required_js_list: [js1_url]
}); });
stop(); stop();
gadget.declareGadget(html_url, $('#qunit-fixture')) gadget.declareGadget(html_url)//, $('#qunit-fixture'))
.done(function (new_gadget) { .then(function (new_gadget) {
ok(false); ok(false);
}) })
.fail(function (jqXHR, textStatus) { .fail(function (e) {
equal(jqXHR.status, 404); ok(true);
equal(textStatus, "error");
}) })
.always(function () { .always(function () {
start(); start();
server.restore();
mock.verify();
mock.restore();
}); });
server.respond();
}); });
test('Do not load gadget dependency twice', function () { test('Do not load gadget dependency twice', function () {
// Check that dependencies are not reloaded if 2 gadgets are created // Check that dependencies are not reloaded if 2 gadgets are created
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
server = sinon.fakeServer.create(), server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test2.html', html_url = 'https://example.org/files/qunittest/test254.html',
js1_url = "data:application/javascript;base64," + js1_url = "data:application/javascript;base64," +
window.btoa( window.btoa(
"$('#qunit-fixture').find('div').first().append('youhou');" "$('#qunit-fixture').find('div').first().append('youhou');"
), ),
mock, mock;
spy;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, { server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "raw html"]); }, "raw html"]);
spy = this.spy($, "ajax"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTMLDocument").once().returns({
mock = this.mock(renderJS, "parseGadgetHTML");
mock.expects("parseGadgetHTML").once().withArgs("raw html").returns({
required_js_list: [js1_url] required_js_list: [js1_url]
}); });
stop(); stop();
$('#qunit-fixture').html("<div></div><div></div>"); $('#qunit-fixture').html("<div></div><div></div>");
gadget.declareGadget(html_url, $('#qunit-fixture').find("div").last()) gadget.declareGadget(html_url)//, $('#qunit-fixture').find("div").last()[0])
.always(function () { .always(function () {
equal($('#qunit-fixture').html(), equal($('#qunit-fixture').html(),
"<div>youhou</div><div></div>"); "<div>youhou</div><div></div>");
gadget.declareGadget(html_url, $('#qunit-fixture').find("div").last()) return gadget.declareGadget(html_url);//,
.done(function (new_gadget) { //$('#qunit-fixture').find("div").last()[0]
equal($('#qunit-fixture').html(), // );
"<div>youhou</div><div></div>"); })
ok(spy.calledTwice, "Ajax count " + spy.callCount); .then(function (new_gadget) {
equal(spy.firstCall.args[0], html_url, "First ajax call"); equal($('#qunit-fixture').html(),
deepEqual(spy.secondCall.args[0], { "<div>youhou</div><div></div>");
"cache": true, })
"dataType": "script", .fail(function () {
"url": js1_url, ok(false);
}, "Second ajax call"); })
}) .always(function () {
.fail(function () { start();
ok(false); server.restore();
}) mock.verify();
.always(function () { mock.restore();
start();
});
}); });
server.respond();
}); });
test('Load 2 concurrent gadgets in parallel', function () { test('Load 2 concurrent gadgets in parallel', function () {
// Check that dependencies are loaded once if 2 gadgets are created // Check that dependencies are loaded once if 2 gadgets are created
var gadget = new RenderJSGadget(), var gadget = new RenderJSGadget(),
server = sinon.fakeServer.create(), server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test2.html', html_url = 'https://example.org/files/qunittest/test987.html',
mock, mock;
spy;
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, { server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html", "Content-Type": "text/html",
}, "raw html"]); }, "raw html"]);
spy = this.spy($, "ajax"); mock = sinon.mock(renderJS, "parseGadgetHTMLDocument");
mock.expects("parseGadgetHTMLDocument").once().returns({});
mock = this.mock(renderJS, "parseGadgetHTML"); stop();
mock.expects("parseGadgetHTML").once().withArgs("raw html").returns({}); RSVP.all([
gadget.declareGadget(html_url),// $('#qunit-fixture')),
gadget.declareGadget(html_url)//, $('#qunit-fixture'))
])
.then(function () {
ok(true);
})
.always(function () {
// Check that only one request has been done.
start();
mock.verify();
mock.restore();
server.restore();
});
});
test('Wait for ready callback before returning', function () {
// Subclass RenderJSGadget to not pollute its namespace
var called = false,
gadget = new RenderJSGadget(),
server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test98.html';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html",
}, "<html><body></body></html>"]);
stop(); stop();
$.when( renderJS.declareGadgetKlass(html_url)
gadget.declareGadget(html_url, $('#qunit-fixture')), .then(function (Klass) {
gadget.declareGadget(html_url, $('#qunit-fixture')) // Create a ready function
).always(function () { Klass.ready(function () {
// Check that only one request has been done. return RSVP.delay(50).then(function () {
ok(spy.calledOnce, "Ajax count " + spy.callCount); // Modify the value after 50ms
equal(spy.firstCall.args[0], html_url, "First ajax call"); called = true;
start(); });
}); });
server.respond(); return gadget.declareGadget(html_url);//, $('#qunit-fixture')[0]);
})
.then(function () {
ok(called);
})
.fail(function (e) {
console.warn(e);
ok(false);
})
.always(function () {
start();
server.restore();
});
});
test('Can take a DOM element options', function () {
// Subclass RenderJSGadget to not pollute its namespace
var gadget = new RenderJSGadget(),
server = sinon.fakeServer.create(),
html_url = 'https://example.org/files/qunittest/test98.html';
server.autoRespond = true;
server.autoRespondAfter = 5;
server.respondWith("GET", html_url, [200, {
"Content-Type": "text/html",
}, "<html><body><p>foo</p></body></html>"]);
$('#qunit-fixture').empty();
stop();
renderJS.declareGadgetKlass(html_url)
.then(function (Klass) {
return gadget.declareGadget(html_url, {element: $('#qunit-fixture')[0]});
})
.then(function () {
equal($('#qunit-fixture').html(), '<p>foo</p>');
})
.fail(function (e) {
console.warn(e);
ok(false);
})
.always(function () {
start();
server.restore();
});
});
/////////////////////////////////////////////////////////////////
// RenderJSGadget bootstrap
/////////////////////////////////////////////////////////////////
module("RenderJSGadget bootstrap");
// module("RenderJSGadget bootstrap", {
// setup: function () {
// renderJS.clearGadgetKlassList();
// }
// });
test('Check that the root gadget is cleanly implemented', function () {
stop();
console.log(root_gadget_klass);
root_gadget_defer.promise
.then(function (root_gadget) {
// Check instance
equal(root_gadget.path, window.location.href);
equal(root_gadget.title, document.title);
deepEqual(root_gadget.interface_list, []);
deepEqual(root_gadget.required_css_list, ["../lib/qunit/qunit.css"]);
deepEqual(root_gadget.required_js_list, [
"../lib/rsvp/rsvp.js",
"../lib/jquery/jquery.js",
"../lib/qunit/qunit.js",
"../lib/sinon/sinon.js",
"../../lib/jschannel/jschannel.js",
"../renderjs.js",
"renderjs_test.js",
]);
equal(root_gadget.element.outerHTML, document.body.outerHTML);
// Check klass
equal(root_gadget.constructor.prototype.path, window.location.href);
equal(root_gadget.constructor.prototype.title, document.title);
deepEqual(root_gadget.constructor.prototype.interface_list, []);
deepEqual(root_gadget.constructor.prototype.required_css_list,
["../lib/qunit/qunit.css"]);
deepEqual(root_gadget.constructor.prototype.required_js_list, [
"../lib/rsvp/rsvp.js",
"../lib/jquery/jquery.js",
"../lib/qunit/qunit.js",
"../lib/sinon/sinon.js",
"../../lib/jschannel/jschannel.js",
"../renderjs.js",
"renderjs_test.js",
]);
var html = root_gadget.constructor.template_element.outerHTML;
ok(/^<div>\s*<h1 id="qunit-header">/.test(html), html);
})
.fail(function (e) {
ok(false, e);
})
.always(function () {
start();
});
}); });
}(document, jQuery, renderJS, QUnit, sinon)); }(document, jQuery, renderJS, QUnit, sinon));
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